diff --git a/.gitignore b/.gitignore index f6db2ed..c2c4ecc 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +.noseids # Translations *.mo @@ -90,6 +91,13 @@ Thumbs.db .venv env/ venv/ +venv27/ +venv3/ +venv36/ +venv37/ +venv38/ +venv35/ +venv34/ ENV/ env.bak/ venv.bak/ @@ -107,6 +115,12 @@ venv.bak/ # mypy .mypy_cache/ -# casts -casts/casts -casts/gifs +# vscode +.vscode + + +# tmp stuff +tmp/ + +# Invoke config +invoke.yaml diff --git a/.travis.yml b/.travis.yml index 3b5c703..4d48e2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" @@ -10,6 +9,7 @@ install: - "python -m pip install -U pip" - "pip install --upgrade setuptools" - "pip install -r requirements.txt" + - "pip install -r requirements-dev.txt" script: - - nosetests -v -s --logging-clear-handlers --with-doctest + - invoke tests diff --git a/CHANGELOG b/CHANGELOG index e94dc81..0fbea3a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,13 +2,21 @@ Release Notes ============= +1.0.0 (Unreleased) +------------------ + +* Add tests package +* Rewrite Construct with multisite studios in mind + 0.1.21 (2018-06-15) +------------------- * Context.from_path - Exclude all entry keys 0.1.20 (2018-05-22) +------------------- * Add support for Collection Entry type @@ -23,11 +31,13 @@ Release Notes 0.1.14 +------ * Adjust availability of new.template, file.* and publish.file 0.1.13 +------ * Add more info to STATUSES and TASK_TYPES config vars * Fixed mac os mangled some paths causes fsfs search to fail @@ -35,16 +45,19 @@ Release Notes 0.1.11 +------ * Fixed fsfs was not setup prior to pulling context from env vars 0.1.10 +------ * *construct push* now goes to the shortest path found 0.1.0 +----- * Include cli in main package * Improved Task api diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..f96e7eb --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1 @@ +Dan Bradham (danielbradham@gmail.com) diff --git a/README.rst b/README.rst index b58bf4e..1bf3337 100644 --- a/README.rst +++ b/README.rst @@ -9,39 +9,16 @@ ========= construct ========= -An extensible API for creative project and asset management. +An extensible API for creative projects and asset libraries. Features ======== - - Manage Projects on your file system + - Manage Projects and Asset Libraries + - Create your own Extensions and Actions - Integrate with your Creative Software - - Manage Workspaces and Publishes - - Folder and Path Templating (fsfs_ and Lucidity_) - - Contextual API - - Extensions, Actions, and Tasks - - Rock solid CLI - - + manage and navigate your projects - + automatically generate commands from Actions - -Core Packages -============= - -+----------------------+------------------------------------------------+ -| package | description | -+======================+================================================+ -| construct_cpenv_ | Cpenv Integration for env management | -+----------------------+------------------------------------------------+ -| construct_launcher_ | Application launcher | -+----------------------+------------------------------------------------+ -| construct_maya_ | Autodesk Maya integration | -+----------------------+------------------------------------------------+ -| construct_nuke_ | The Foundry Nuke integration | -+----------------------+------------------------------------------------+ -| construct_ui_ | Graphical user interface | -+----------------------+------------------------------------------------+ + - Manage Work files and Publish files Installation @@ -57,12 +34,9 @@ Documentation `Visit the Full Documentation for more info and API Documentation `_ -.. _construct_cli: https://github.com/construct-org/construct_cli -.. _construct_cpenv: https://github.com/construct-org/construct_cpenv -.. _construct_templates: https://github.com/construct-org/construct_templates -.. _construct_launcher: https://github.com/construct-org/construct_launcher -.. _construct_maya: https://github.com/construct-org/construct_maya -.. _construct_nuke: https://github.com/construct-org/construct_nuke -.. _construct_ui: https://github.com/construct-org/construct_ui -.. _fsfs: https://github.com/danbradham/fsfs -.. _Lucidity: https://gitlab.com/4degrees/lucidity +Contribute +========== + + - Fork this repository + - Create a new branch + - Make a pull request diff --git a/appveyor.yml b/appveyor.yml index 0f2a2b5..36f1996 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,6 @@ build: off environment: matrix: - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python34-x64" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" @@ -14,7 +13,8 @@ install: - python -m pip install -U pip - pip install --upgrade setuptools - pip install -r requirements.txt + - pip install -r requirements-dev.txt - pip install nose test_script: - - nosetests -v -s --logging-clear-handlers --with-doctest + - invoke tests diff --git a/bin/construct.sh b/bin/construct similarity index 87% rename from bin/construct.sh rename to bin/construct index 5995fb0..9c01f04 100644 --- a/bin/construct.sh +++ b/bin/construct @@ -1,14 +1,18 @@ #!/bin/bash + construct () { - py_entry_point="pyconstruct" + THIS="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + py_entry_point="$THIS/pyconstruct" + _tmpdir=$(mktemp -d) export SCRIM_AUTO_WRITE="${SCRIM_AUTO_WRITE:=1}" export SCRIM_PATH="${SCRIM_PATH:=$_tmpdir/scrim_out.sh}" export SCRIM_SCRIPT="${0}" export SCRIM_SHELL="${SCRIM_SHELL:=bash}" export SCRIM_DEBUG="${SCRIM_DEBUG:=0}" + debug="test $SCRIM_DEBUG == 1" $debug && echo "Variables:" @@ -23,7 +27,7 @@ construct () { if [ -e "$SCRIM_PATH" ]; then - source "$SCRIM_PATH" + source $SCRIM_PATH if [ $? -ne 0 ]; then echo "[scrim] error executing:" @@ -43,6 +47,7 @@ construct () { unset SCRIM_SHELL unset SCRIM_DEBUG unset debug + unset THIS rm -rf $_tmpdir } diff --git a/casts/.config/asciinema/install-id b/casts/.config/asciinema/install-id deleted file mode 100644 index 3064ab2..0000000 --- a/casts/.config/asciinema/install-id +++ /dev/null @@ -1 +0,0 @@ -e74024c0-3f88-11e8-9c80-bc83850e38b2 diff --git a/casts/.tmux.conf b/casts/.tmux.conf deleted file mode 100644 index 65fe5f5..0000000 --- a/casts/.tmux.conf +++ /dev/null @@ -1 +0,0 @@ -set-option -g status off diff --git a/casts/README.rst b/casts/README.rst deleted file mode 100644 index bf6bc12..0000000 --- a/casts/README.rst +++ /dev/null @@ -1,47 +0,0 @@ -===== -casts -===== - - -Record CLI demos using spielbash/asciinema -========================================== - -Build it... - -.. code-block:: console - - docker build -f record.Dockerfile -t record_casts . - - -Run it... - -.. code-block:: console - - docker run --rm -it -v $PWD:/data record_casts --width 120 --height 40 - - -Convert asciicasts to gif using asciicast2gif -============================================= - -Build it... - -.. code-block:: console - - docker build -f convert.Dockerfile -t convert_casts . - - -Run it... - -.. code-block:: console - - docker run --rm -it -v %cd%:/data convert_casts - - -Run em back to back on Windows -============================== - -Build both the record and convert Dockerfiles. Then run... - -.. code-block:: console - - build diff --git a/casts/convert.Dockerfile b/casts/convert.Dockerfile deleted file mode 100644 index 0305265..0000000 --- a/casts/convert.Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM asciinema/asciicast2gif - - -# Install convert CMD - -ADD convert.sh /root/convert.sh -RUN chmod +x /root/convert.sh - - -# Install font - -RUN apt-get update -RUN apt-get -y install unifont -RUN fc-cache -f -v - - -# Add unifont to head of font-family for asciinema-player.css - -RUN sed -i 's/font-family: C/font-family: unifont, C/g' /app/page/asciinema-player.css - - -ENTRYPOINT ["/root/convert.sh"] diff --git a/casts/convert.sh b/casts/convert.sh deleted file mode 100644 index 9e2edb3..0000000 --- a/casts/convert.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -mkdir -p gifs - -for filename in casts/*.cast; do - - output="gifs/$(basename "$filename" .cast).gif" - - echo "asciicast2gif $@ $filename $output" - /app/asciicast2gif "$@" $filename $output - echo - -done diff --git a/casts/record.Dockerfile b/casts/record.Dockerfile deleted file mode 100644 index 2f57f9c..0000000 --- a/casts/record.Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -From asciinema/asciinema - - -# Install pip - -RUN apt-get update -RUN apt-get install -y python3-pip -RUN apt-get install -y git -RUN apt-get install -y tmux - -RUN python3 -m pip install pip --upgrade -RUN python3 -m pip install wheel - - -# Install construct - -RUN pip install git+git://github.com/construct-org/construct_setup.git -I --process-dependency-links --trusted-host github.com -RUN echo "source /usr/local/bin/construct.sh" >> /root/.bashrc - - -# Install spielbash - -RUN pip install git+git://github.com/danbradham/spielbash.git - - -# Configure tmux - -ADD .tmux.conf /root/.tmux.conf - - -# Install record CMD - -ADD record.sh /src/record.sh -RUN chmod +x /src/record.sh - - -# Setup data volume - -WORKDIR /data -VOLUME ["/data"] - - -# Set PS1 -RUN echo 'export PS1="\w> "' >> /root/.bashrc - - -ENTRYPOINT ["/src/record.sh"] diff --git a/casts/record.sh b/casts/record.sh deleted file mode 100644 index 1b71499..0000000 --- a/casts/record.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -mkdir -p casts - -for filename in scripts/*.yaml; do - - output="casts/$(basename "$filename" .yaml).cast" - - echo "spielbash --script $filename --output $output $@" - spielbash --script $filename --output $output "$@" - echo - -done diff --git a/casts/scripts/new_project.yaml b/casts/scripts/new_project.yaml deleted file mode 100644 index 5bef597..0000000 --- a/casts/scripts/new_project.yaml +++ /dev/null @@ -1,8 +0,0 @@ -title: Create a new project -scenes: - - name: go home note - action: "# first we'll navigate to our root" - - name: go home - action: construct home - - name: new project - action: construct new.project --root=my_project diff --git a/casts/tasks.py b/casts/tasks.py deleted file mode 100644 index a698ecd..0000000 --- a/casts/tasks.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/bin/bash -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import argparse -import os -import sys - - -class record(object): - '''Uses spielbash and asciinema to record cli demos stored in scripts/. - ''' - - def __call__(self, args): - build_cmd = ( - 'docker build -q -f record.Dockerfile -t record_casts .' - ) - os.system(build_cmd) - cmd = ( - 'docker run --rm -it -v %cd%:/data record_casts ' - '--width {width} ' - '--height {height} ' - '--speed {speed} ' - '--readtime {readtime}' - ).format(**args.__dict__) - os.system(cmd) - - def parser(self): - parser = argparse.ArgumentParser( - 'tasks record', - add_help=False, - description=self.__doc__ - ) - parser.add_argument( - '-w', '--width', - type=int, - help='Console width (default: 120)', - default=120, - required=False - ) - parser.add_argument( - '-h', '--height', - type=int, - help='Console height (default: 40)', - default=40, - required=False - ) - parser.add_argument( - '-s', '--speed', - type=float, - help='Typing Speed (default: 0.1)', - default=0.1, - required=False - ) - parser.add_argument( - '-r', '--readtime', - type=float, - help='Time to wait between scenes (default: 1.0)', - default=1.0, - required=False - ) - parser.add_argument( - '--help', - help='show this help message and exit', - action='store_true' - ) - return parser - - -class convert(object): - '''Uses asciicast2gif to convert all cast files in casts/ to gif files. - Run after record task. Accepts the same arguments as asciicast2gif.''' - - def __call__(self, args): - build_cmd = ( - 'docker build -q -f convert.Dockerfile -t convert_casts .' - ) - os.system(build_cmd) - cmd = ( - 'docker run --rm -it -v %cd%:/data convert_casts ' - '-w {width} ' - '-h {height} ' - '-s {speed} ' - '-t {theme} ' - '-S {scale}' - ).format(**args.__dict__) - os.system(cmd) - - def parser(self): - parser = argparse.ArgumentParser( - 'tasks convert', - add_help=False, - description=self.__doc__ - ) - parser.add_argument( - '-w', '--width', - type=int, - help='clip terminal to specified number of columns (width: 120)', - default=120, - required=False - ) - parser.add_argument( - '-h', '--height', - type=int, - help='clip terminal to specified number of rows (height: 40)', - default=40, - required=False - ) - parser.add_argument( - '-t', '--theme', - help='color theme, one of: asciinema, tango, solarized-dark, solarized-light, monokai (default: asciinema)', - default='asciinema', - required=False - ) - parser.add_argument( - '-S', '--scale', - help='image scale / pixel density (default: 2)', - type=int, - default=2, - required=False - ) - parser.add_argument( - '-s', '--speed', - type=int, - help='animation speed (default: 1)', - default=1, - required=False - ) - parser.add_argument( - '--help', - help='show this help message and exit', - action='store_true' - ) - return parser - - -class upload(object): - '''Uploads all casts in casts/ to asciinema.org''' - - def __call__(self, args): - cmd = ( - 'docker run --rm -it ' - '-v %cd%/casts:/root/casts -v %cd%/.config:/root/.config ' - 'asciinema/asciinema ' - 'asciinema upload casts/{file}' - ) - for f in os.listdir('casts'): - if f.endswith('.cast'): - os.system(cmd.format(file=f)) - - def parser(self): - parser = argparse.ArgumentParser( - 'tasks upload', - description=self.__doc__ - ) - return parser - - -def main(): - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument( - 'command', - choices=['record', 'convert', 'upload'] - ) - args, extra_args = parser.parse_known_args() - - cmd = getattr(sys.modules[__name__], args.command)() - cmd_parser = cmd.parser() - args = cmd_parser.parse_args(extra_args) - if 'help' in args and args.help: - cmd_parser.print_help() - sys.exit(1) - cmd(args) - - -if __name__ == '__main__': - main() diff --git a/construct/__init__.py b/construct/__init__.py index 3928f7c..3f67a25 100644 --- a/construct/__init__.py +++ b/construct/__init__.py @@ -1,13 +1,34 @@ # -*- coding: utf-8 -*- + from __future__ import absolute_import +# Local imports +from . import api +from .constants import DEFAULT_API_NAME + + +# Package metadata +__version__ = '0.2.0' __title__ = 'construct' -__description__ = 'Creative project and asset management' -__version__ = '0.1.22' +__description__ = 'Extensible api for projects and asset libraries' __author__ = 'Dan Bradham' __email__ = 'danielbradham@gmail.com' -__license__ = 'MIT' __url__ = 'https://github.com/construct-org/construct' -from construct.api import * +def API(name=DEFAULT_API_NAME, **kwargs): + '''Wraps :class:`construct.api.API` and maintains a cache of API objects. + + This allows you to get the same API object each time you call API with the + same name. + + Examples: + >>> import construct + >>> api = construct.API() + >>> api is construct.API() + True + ''' + + if name not in api._cache: + api._cache[name] = api.API(name, **kwargs) + return api._cache[name] diff --git a/construct/action.py b/construct/action.py deleted file mode 100644 index 678c488..0000000 --- a/construct/action.py +++ /dev/null @@ -1,348 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -__all__ = [ - 'Action', - 'ActionCollector', - 'ActionProxy', - 'get_action_identifier', - 'group_actions', - 'is_action', - 'is_action_type', - 'sort_actions', -] - -import abc -import logging -from operator import attrgetter -from collections import OrderedDict -from construct import types, actionparams, signals -from construct.compat import basestring -from construct.constants import ACTION_SIGNALS -from construct.utils import missing, classproperty -from construct.actionrunner import ActionRunner -from construct.tasks import sort_tasks, CtxAction -from construct.errors import ActionUnavailable - - -_log = logging.getLogger(__name__) - - -class Action(types.ABC): - '''Action Base Class - - Attributes: - label (str): Nice name used for labeling like: 'New Project' - identifier (str): Used to find and run the action like: 'new.project' - description (str): Description of what the action does - parameters (dict or staticmethod): Description of parameters for - validation like: - - { - 'str_param': { - 'label': 'Str Param', - 'type': str, - 'required': True, - }, - 'int_param': { - 'label': 'Int Param', - 'type': int, - 'required': False, - 'default': 1 - }, - ... - } - - ''' - - runner_cls = ActionRunner - parameters = None - suppress_signals = False - - @abc.abstractproperty - def label(self): - return NotImplemented - - @abc.abstractproperty - def identifier(self): - return NotImplemented - - @classproperty - def description(cls): - return cls.__doc__.split('\n')[0] - - @classproperty - def long_description(cls): - from textwrap import dedent - lines = cls.__doc__.split('\n') - first = lines[0] + '\n' - rest = '' - if len(lines) > 1: - rest = dedent('\n'.join(lines[1:])) - return first + rest - - def _returns(self): - '''Called by ActionProxy to extract a return value from the Action's - Context after run. This function calls Action.returns passing along - the ActionContext when available. If Action.returns is not set - all the artifacts from an Action run are returned. - - Examples: - - from construct import Action - from construct.tasks import artifact - - class NewProject(Action): - ... - returns = artifact('project') - ''' - - return_getter = getattr(self, 'returns', None) - if return_getter: - if isinstance(return_getter, CtxAction): - return return_getter.get(self.ctx) - else: - return return_getter(self.ctx) - else: - return self.ctx.artifacts - - @classmethod - def _available(cls, ctx=missing): - if ctx is missing: - return True - if callable(cls.available): - return cls.available(ctx) - return cls.available - - @abc.abstractmethod - def available(ctx): - ''' - Return True if this action is available in the provided Context. Must - be a classmethod or staticmethod. - - Arguments: - ctx (Context): Context class - ''' - - @classmethod - def params(cls, ctx): - if callable(cls.parameters): - return cls.parameters(ctx) - return cls.parameters or {} - - def __init__(self, *args, **kwargs): - from construct.api import get_context - from construct.actioncontext import ActionContext - - # Get base context to build ActionContext from - ctx = kwargs.pop('ctx', get_context()) - - # Validate params - params = self.params(ctx) - actionparams.validate(params) - - # Combine kwargs with action parameter defaults - action_kwargs = actionparams.get_defaults(params) - action_kwargs.update(kwargs) - - # Create action context - self.ctx = ActionContext(self, args, action_kwargs, ctx) - - # Validate kwargs against params - actionparams.validate_kwargs(params, self.ctx.kwargs) - - # Create action runner - self.runner = self.runner_cls(self, self.ctx) - - def __call__(self): - return self.runner.run() - - def __getattr__(self, attr): - return self.ctx.__dict__.get(attr) - - def retry_group(self, priority): - with signals.suppressed_when(self.suppress_signals, *ACTION_SIGNALS): - return self.runner.retry_group(priority) - - def run_group(self, priority): - with signals.suppressed_when(self.suppress_signals, *ACTION_SIGNALS): - return self.runner.run_group() - - def run(self): - with signals.suppressed_when(self.suppress_signals, *ACTION_SIGNALS): - return self.runner.run() - - -def is_action_type(obj): - return ( - obj is not Action and - isinstance(obj, type) and - issubclass(obj, Action) - ) - - -def is_action(obj): - return isinstance(obj, Action) - - -def get_action_identifier(action_or_identifier): - - identifier = getattr(action_or_identifier, 'identifier', None) - if identifier: - return identifier - - if isinstance(action_or_identifier, basestring): - return action_or_identifier - - raise RuntimeError( - 'Can not determine identifier of %s' % action_or_identifier - ) - - -class ActionCollector(object): - '''Collects Actions and their associated tasks from the currently - available Extensions. - ''' - - def __init__(self, extension_collector): - self._extensions = extension_collector - - def __iter__(self): - for action in sort_actions(self.collect().values()): - yield action - - def __getattr__(self, identifier): - return self.get(identifier) - - def get(self, identifier, ctx=None): - from construct.api import get_context - ctx = ctx or get_context() - try: - action = self.collect(missing)[identifier] - except KeyError: - _log.error('Action not found: %s', identifier) - return - else: - if not action._available(ctx): - raise ActionUnavailable('Action unavaibale in current context') - return action - - def collect(self, ctx=None): - '''Collect all actions in the given context. If no context is provided - return ALL actions. - - Arguments: - ctx: Context instance - used to determine action availability - - Returns: - dict - (Action.identifier, Action) key value pairs - ''' - - from construct.api import get_context - ctx = ctx or get_context() - actions = {} - for name, extension in self._extensions.by_name.items(): - if extension.enabled and extension._available(ctx): - ext_actions = extension.get_actions(ctx) - for identifier, action in ext_actions.items(): - if identifier in actions: - _log.warning( - 'Action identifier collision: %s <-> %s', - actions[identifier], action - ) - else: - actions[identifier] = action - - return actions - - def collect_tasks(self, action_or_identifier, ctx=None): - '''Collect all the tasks for a given Action or Action identifier. - - Arguments: - action_or_identifier: Action class or Action.identifier string - ctx: Context instance - - Returns: - [Task...] - List of Task instances in priority order - ''' - - from construct.api import get_context - ctx = ctx or get_context() - identifier = get_action_identifier(action_or_identifier) - - tasks = [] - for name, extension in self._extensions.by_name.items(): - if extension.enabled and extension._available(ctx): - more_tasks = extension.get_tasks(identifier, ctx) - tasks.extend(more_tasks) - - return sort_tasks(tasks) - - -class ActionProxy(object): - '''Proxy object that allows you to call Actions as if they were functions. - ''' - - def __init__(self, identifier): - self.identifier = identifier - - @property - def action(self): - from construct import actions - return actions.get(self.identifier) - - def instance(self, **kwargs): - return self.action(**kwargs) - - def __repr__(self): - return repr(self.action) - - def __str__(self): - return str(self.action) - - def __getattr__(self, attr): - return getattr(self.action, attr) - - def __call__(self, **kwargs): - a = self.action(**kwargs) - a.run() - return a._returns() - - -def sort_actions(actions): - '''Sort the given actions by identifier''' - - return sorted(actions, key=attrgetter('identifier')) - - -def group_actions(actions): - '''Group sorted actions in a tree structure. - - Returns: - OrderedDict suitable for creating a nested menu - ''' - - if isinstance(actions, dict): - actions = sort_actions(actions.values()) - else: - actions = sort_actions(actions) - - groups = OrderedDict() - for action in actions: - parts = action.identifier.split('.') - if len(parts) == 1: - groups.setdefault('', []) - groups[''].append(action) - else: - nodes = parts[:-1] - section = None - for node in nodes: - if section is None: - section = groups.setdefault(node, OrderedDict()) - section.setdefault('', []) - else: - section = section.setdefault(node, OrderedDict()) - section.setdefault('', []) - section[''].append(action) - - return groups diff --git a/construct/actioncontext.py b/construct/actioncontext.py deleted file mode 100644 index 81ddc76..0000000 --- a/construct/actioncontext.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from construct.context import Context -from construct.tasks import group_tasks -from construct import types, actionparams -from construct.constants import WAITING - - -class ActionContext(Context): - - def __init__(self, action, args, kwargs, ctx=None): - from construct.api import actions, get_context - - if ctx is None: - ctx = get_context() - super(ActionContext, self).__init__(**ctx.__dict__) - - self.action = action - self.status = WAITING - self.tasks = actions.collect_tasks(action, self) - self.task_groups = group_tasks(self.tasks) - self.priorities = list(self.task_groups.keys()) - self.results = {t.identifier: None for t in self.tasks} - self.requests = {} - self.artifacts = types.Namespace() - self.store = types.Namespace() - self.args = args - self.kwargs = actionparams.get_defaults(action.params(ctx)) - self.kwargs.update(kwargs) diff --git a/construct/actionparams.py b/construct/actionparams.py deleted file mode 100644 index f086089..0000000 --- a/construct/actionparams.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -__all__ = ['validate', 'validate_kwargs', 'get_defaults'] - -import string -from construct.errors import ParameterError, ArgumentError -from fstrings import f - -REQUIRED_OPTIONS = ['type', 'label'] -OPTIONS = ['type', 'label', 'default', 'required', 'options'] -# TODO: Add an additional option to handle coercions? -# coercions=[(TYPE, CONVERTER)...] -VALID_CHARACTERS = string.ascii_lowercase + string.digits + '_' - - -def validate(parameters): - '''Validate Action parameters dict - - Arguments: - parameters (dict): describes parameters of a callable: - - { - 'str_param': { - 'label': 'Str Param', - 'type': str, - 'required': True, - 'validator': lambda x: 1 < len(x) < 36, - 'help': 'This is a required string parameter' - }, - 'int_param': { - 'label': 'Int Param', - 'type': int, - 'required': False, - 'default': 1, - 'help': 'This is an unrequired int parameter' - }, - ... - } - - Returns: - True if parameters are valid - - Raises: - ParameterError: describing invalid parameter - ''' - - for name, options in parameters.items(): - - chars = set(name) - invalid_chars = chars - set(VALID_CHARACTERS) - if invalid_chars: - raise ParameterError( - name, ' contains invalid chars: ', ''.join(invalid_chars) - ) - - opt_names = set(options.keys()) - missing_options = set(REQUIRED_OPTIONS) - opt_names - if missing_options: - raise ParameterError( - name, ' missing required options: ', list(missing_options) - ) - - if 'default' in options: - default = options['default'] - if callable(default): - pass - elif not isinstance(default, options['type']): - raise ParameterError( - '{} default value, {}, does not match type, {}'.format( - name, default, options['type'] - ) - ) - - validator = options.get('validator', None) - if validator and not callable(validator): - raise ParameterError( - 'Validator must be callable: ', name - ) - - return True - - -def validate_kwargs(parameters, kwargs): - '''Validate kwargs using Action parameters dict: - - >>> parameters = {'arg': {'name': 'Arg', 'default': 1.0, 'type': float}} - >>> kwargs = {'arg': 10.0} - >>> validate_kwargs(parameters, kwargs) - True - - Arguments: - parameters (dict): describes parameters of a callable: - kwargs (dict): function arguments as keyword arguments: - - Returns: - True - - Raises: - ArgumentError: describing why kwargs is invalid - ''' - - if not parameters and kwargs: - raise ArgumentError(f('Got additional arguments: {kwargs}')) - - for name, options in parameters.items(): - - validator = options.get('validator', None) - required = options.get('required', False) - type_ = options['type'] - value = kwargs.get(name, None) - valid_values = options.get('options', None) - - if value is None and required: - raise ArgumentError(f('Missing required argument: {name}')) - elif value is None and not required: - continue - - if not isinstance(value, options['type']): - raise ArgumentError(f('{name} must be {type_} not {value}.')) - - if valid_values: - exc_msg = f('{name} must be one of: {valid_values}') - if value not in valid_values: - raise ArgumentError(exc_msg) - - if validator: - exc_msg = f('{name} failed custom validator: {value}') - try: - if validator(value) is False: - raise ArgumentError(exc_msg) - except: - raise ArgumentError(exc_msg) - - return True - - -def get_defaults(parameters): - '''Extract default values from parameters dict. - - >>> parameters = {'arg': {'name': 'Arg', 'default': 1.0}} - >>> get_defaults(parameters) - {'arg': 1.0} - - Arguments: - parameters (dict): describes parameters of a callable: - - Returns: - dict: Containing name, default value pairs - ''' - - defaults = {} - for name, options in parameters.items(): - try: - default = options['default'] - if callable(default): - default = default() - defaults[name] = default - except KeyError: - continue - - return defaults diff --git a/construct/actionrunner.py b/construct/actionrunner.py deleted file mode 100644 index 644b623..0000000 --- a/construct/actionrunner.py +++ /dev/null @@ -1,344 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -__all__ = ['TaskGroup', 'ActionRunner'] - -import logging -import six -from construct.types import Stack -from construct import signals -from construct.constants import * -from construct.errors import ( - TimeoutError, - ValidationError, - Abort, - Fail, - Pause, - Confirm, - Skip, - Disable -) -from construct.tasks import AsyncRequest -from fstrings import f - - -_log = logging.getLogger(__name__) - - -class TaskGroup(object): - - def __init__(self, priority, tasks, runner): - self.priority = priority - self.tasks = tasks - self.runner = runner - self._exc = None - self._status = WAITING - - def set_exception(self, exc): - self.set_status(FAILED) - self._exc = exc - - def get_exception(self): - return self._exc - - def raise_exception(self): - if not self._exc: - return - six.reraise(*self._exc) - - def set_status(self, status): - if self._status == status: - return - last_status = self._status - self._status = status - signals.send('group.status.changed', self, last_status, status) - - def get_status(self): - return self._status - - @property - def waiting(self): - return self._status == WAITING - - @property - def running(self): - return self._status == RUNNING - - @property - def success(self): - return self._status == SUCCESS - - @property - def failed(self): - return self._status == FAILED - - @property - def done(self): - return self._status in DONE_STATUSES - - def push(self): - for task in self.tasks: - request = self.runner._get_request(task) - self.runner._remove_from_stacks(request) - self.runner._waiting.push(request) - - self.set_status(PENDING) - - def reset(self): - for task in self.tasks: - request = self.runner._get_request(task) - self.runner._remove_from_stacks(request) - self.runner.ctx.requests.pop(request.task.identifier, None) - - self.set_status(WAITING) - - -class ActionRunner(object): - - def __init__(self, action, ctx): - self.action = action - self.ctx = ctx - self._groups = { - p: TaskGroup(p, t, self) - for p, t in self.ctx.task_groups.items() - } - self._waiting = Stack() - self._ready = Stack() - self._failed = Stack() - self._success = Stack() - self._skipped = Stack() - self._disabled = Stack() - self._stacks = [ - self._waiting, - self._ready, - self._failed, - self._success, - self._skipped - ] - - def _remove_from_stacks(self, request): - for stack in self._stacks: - try: - stack.remove(request) - except ValueError: - continue - - def _fail_request(self, request): - self._failed.push(request) - self.ctx.results[request.task.identifier] = request._exc - group = self._get_group(request.task.priority) - group.set_exception(request._exc) - - def _get_group(self, priority): - return self._groups[priority] - - def _get_request(self, task): - '''Create a request object for a Task''' - - if task.identifier not in self.ctx.requests: - self.ctx.requests[task.identifier] = task.request(ctx=self.ctx) - - return self.ctx.requests[task.identifier] - - def _process_waiting(self): - '''Process waiting tasks''' - - nwaiting = len(self._waiting) - for _ in range(nwaiting): - - request = self._waiting.pop() - task = request.task - - if not request.enabled: - self._disabled.push(request) - - elif task.ready(self.ctx): - self._ready.push(request) - - elif task.skip(self.ctx): - request.set_status(SKIPPED) - self._skipped.push(request) - - else: - self._waiting.push(request) - - def _process_ready(self): - '''Process ready tasks''' - - nready = len(self._ready) - for _ in range(nready): - - request = self._ready.pop() - task = request.task - - if task.skip(self.ctx): - request.set_status(SKIPPED) - self._skipped.push(request) - continue - - try: - - get_kwargs = {'propagate': True} - if isinstance(request, AsyncRequest): - get_kwargs['timeout'] = 0 - - result = request.get(**get_kwargs) - - except TimeoutError: - - # AsyncRequest timed out, push it back onto the ready stack - # and check it again next time around - self._ready.push(request) - - except ValidationError: - - # Task failed to validate, future tasks can handle this - # tasks validation errors by requiring failure of this task - # and receiving the result of this task as a parameter - self._fail_request(request) - - except Fail: - - # Task failed but does not want to stop execution - self._fail_request(request) - - except Abort: - - # Task explicitly sent Abort Error - self._fail_request(request) - raise - - except Pause: - - # Task wants to paused execution of the Action - self._success.push(request) - self.ctx.results[task.identifier] = result - raise - - except Confirm: - - # Task has requested user confirmation - self._fail_request(request) - - # TODO: Get user input on Confirm - raise NotImplementedError( - 'Confirm error handling not yet implemented' - ) - - except Skip: - - # Task wants to skip - self._skipped.push(request) - request.set_status(SKIPPED) - - except Disable: - - # Task wants to skip - self._waiting.push(request) - request.set_enabled(False) - request.set_status(DISABLED) - - except Exception: - - # Propagate unrecognized exceptions - self._fail_request(request) - raise - - else: - - # Maybe add support for generator tasks here - # The implementation would be a task that is a generator - # that yields instances of other tasks - # Each task yielded would be added to the _waiting stack - # for future processing - self._success.push(request) - self.ctx.results[task.identifier] = result - - finally: - - # Perform any teardown necessary - pass - - def _process_skipped(self): - - for _ in range(len(self._skipped)): - request = self._skipped.pop() - task = request.task - - if task.ready(self.ctx): - self._ready.push(request) - else: - self._skipped.push(request) - - def _skip_waiting(self): - - nwaiting = len(self._waiting) - for _ in range(nwaiting): - request = self._waiting.pop() - task = request.task - - if task.ready(self.ctx): - self._ready.push(request) - else: - request.set_status(SKIPPED) - self._skipped.push(request) - - def _run_once(self, propagate=True): - - while True: - - self._process_waiting() - - if not self._ready: - self._skip_waiting() - return - - self._process_ready() - self._process_skipped() - - def retry_group(self, priority): - '''Retry TaskGroup of priority''' - - group = self._get_group(priority) - group.reset() - self.run_group(priority) - - def run_group(self, priority): - '''Run TaskGroup of priority''' - - group = self._get_group(priority) - - if group.waiting: - - signals.send('group.before', group) - group.push() - group.set_status(RUNNING) - - try: - - self._run_once() - - finally: - - if not group.failed: - group.set_status(SUCCESS) - - signals.send('group.after', group) - - else: - - raise Exception(f('Task Group already ran: {priority}')) - - def run(self): - '''Run all TaskGroups of the action sequentially''' - - signals.send('action.before', self.ctx) - - try: - - for priority in self.ctx.priorities: - self.run_group(priority) - - finally: - - signals.send('action.after', self.ctx) diff --git a/construct/api.py b/construct/api.py index 249140b..6c27ab9 100644 --- a/construct/api.py +++ b/construct/api.py @@ -1,347 +1,426 @@ # -*- coding: utf-8 -*- + from __future__ import absolute_import -import os -import fsfs +# Standard library imports +import atexit +import inspect import logging +from functools import wraps from logging.config import dictConfig -from itertools import chain -import yaml -from construct.vendor import lucidity -from construct.context import _ctx_stack, _req_stack, Context -from construct.config import Config -from construct.extension import ExtensionCollector, Extension, HostExtension -from construct.action import Action, ActionCollector, ActionProxy -from construct.constants import DEFAULT_LOGGING -from construct.actioncontext import ActionContext -from construct.utils import unipath, ensure_instance -from construct.stats import log_call -from construct.errors import TemplateError - -__all__ = [ - 'Context', - 'config', - 'Config', - 'config_file', - 'extensions', - 'Extension', - 'HostExtension', - 'actions', - 'Action', - 'ActionContext', - 'ActionProxy', - 'init', - 'uninit', - 'set_context', - 'set_context_from_entry', - 'set_context_from_path', - 'get_context', - 'get_request', - 'search', - 'quick_select', - 'get_path_template', - 'get_path_templates', - 'get_template_search_paths', - 'get_templates', - 'get_template', - 'get_host', - 'get_form', - 'new_project', - 'new_sequence', - 'new_shot', - 'new_asset', - 'new_task', - 'new_workspace', - 'new_template', - 'publish', - 'publish_file', - 'save', -] - - -logging.config.dictConfig(DEFAULT_LOGGING) + +# Local imports +from . import schemas +from .compat import Mapping, basestring +from .constants import DEFAULT_LOGGING +from .context import Context +from .errors import ContextError +from .events import EventManager +from .extensions import ExtensionManager +from .io import IO +from .path import Path +from .settings import Settings +from .ui.manager import UIManager +from .utils import ensure_exists, unipath, yaml_dump + + +__all__ = ['API'] + _log = logging.getLogger(__name__) -_initialized = False -_context = None -config = Config() -config_file = None -extensions = ExtensionCollector() -actions = ActionCollector(extensions) - - -@log_call -def init(root=None, host=None, extension_paths=None, logging=None): - '''Initialize Construct''' - - global _initialized - if _initialized: - raise RuntimeError('Construct has already been initialized.') - - # Load configuration - global config_file - config_file = os.environ.get('CONSTRUCT_CONFIG') - if config_file: - with open(config_file, 'r') as f: - config_data = yaml.load(f.read()) - config.update(config_data) - - # Configure logging - dictConfig(logging or config.get('LOGGING', DEFAULT_LOGGING)) +_cache = {} + + +def _on_exit(): + for api in list(_cache.values()): + api.uninit() + _cache.clear() + + +atexit.register(_on_exit) + + +class API(object): + '''The API object is the central object in Construct. When initialized + the API does the following. + + 1. Sends the before_setup event + 2. Initializes a search path from CONSTRUCT_PATH + 3. Loads settings from a construct.yaml file found in the search path + 4. Discovers and loads Extensions + 5. Loads an initial Context from your environment + 6. Sends the after_setup event + + Usually API objects are constructed using the API function available + in the construct module. + + >>> import construct + >>> api = construct.API() + + Arguments: + name (str): Name of the API + path (list): A list of search paths. Defaults to: + CONSTRUCT_PATH + ['~/.construct'] + logging: (dict): Logging configuration. Defaults to: + construct.constants.DEFAULT_LOGGING + + Attributes: + name: Name of this API object. + path: :class:`construct.path.Path` object used for finding resources + and settings files. + settings: :class:`construct.settings.Settings` object used to access + and manipulate settings values. + extensions: :class:`construct.extensions.ExtensionManager` object used + to discover and register :class:`construct.extensions.Extension` s. + context: Access to the current :class:`construct.Context`. + schemas: Access to :mod:`construct.schemas` used for data validation + and normalization. + ''' + + def __init__(self, name=None, **kwargs): + self.name = name + self.events = EventManager() + self.initialized = False + self.path = Path(kwargs.pop('path', None)) + self.settings = Settings(self.path) + self.extensions = ExtensionManager(self) + self.context = Context() + self.schemas = schemas + self.io = IO(self) + self.ui = UIManager(self) + self._logging_dict = kwargs.pop('logging', None) + self._registered_members = {} + self.init() + + def _setup_logging(self): + if self._logging_dict is not None: + dictConfig(self._logging_dict) + else: + dictConfig(self.settings.get('logging', DEFAULT_LOGGING)) - if config_file: - _log.debug('Loaded config: %s' % config_file) - else: - _log.debug('Using default config') + def init(self): - # Setup fsfs entry factory - _log.debug('Configuring fsfs...') - from construct.models import factory - from construct.constants import FSFS_DATA_ROOT, FSFS_DATA_FILE - fsfs.set_entry_factory(factory) - fsfs.set_data_root(FSFS_DATA_ROOT) - fsfs.set_data_file(FSFS_DATA_FILE) + _log.debug('Hi!') + if self.initialized: + _log.error('Construct is already initialized...') + return - # Setup initial context - global _context - _log.debug('Setting initial context...') - _context = Context.from_env() - if root: - _log.debug('Setting root to %s' % root) - _context.root = unipath(root) - elif 'CONSTRUCT_ROOT' in os.environ: - _log.debug('Setting root to %s' % os.environ['CONSTRUCT_ROOT']) - _context.root = unipath(os.environ['CONSTRUCT_ROOT']) - else: - _log.debug('Setting root to %s' % config['ROOT']) - _context.root = unipath(config['ROOT']) + self._setup_logging() - if host: - _context.host = host + _log.debug('Loading events...') + self.events.load() - # Discover extensions - _log.debug('Discovering extensions...') - extension_paths = extension_paths or [] - extensions.discover(*extension_paths) + self.events.send('before_setup', self) - # Register builtins - from construct.builtins import Builtins - extensions.register(Builtins) + _log.debug('Loading path...') + self.path.load() - _log.debug('Initialized!') - _initialized = True + _log.debug('Loading settings...') + self.settings.load() + _log.debug('Loading IO...') + self.io.load() -@log_call -def uninit(): - '''Uninitialize Construct...''' + _log.debug('Loading UI...') + self.ui.load() - global _initialized - if not _initialized: - raise RuntimeError('Construct has not been initialized yet...') + _log.debug('Configuring logging...') + self._setup_logging() - global config - _log.debug('Setting default config...') - config = Config() + _log.debug('Loading extensions...') + self.extensions.load() - global config_file - config_file = None + _log.debug('Loading context...') + self.context.load( + location=self.settings['my_location'], + mount=self.settings['my_mount'], + ) - global _context - _log.debug('Clearing context...') - _context = None + self.initialized = True + _log.debug('Done initializing.') - _log.debug('Removing all extensions...') - extensions.clear() + self.events.send('after_setup', self) - _log.debug('Restoring default fsfs policy...') - fsfs.set_default_policy() + def uninit(self): - _log.debug('Uninitialized!') - _initialized = False + if not self.initialized: + _log.debug('Construct is not initialized...') + return + self.events.send('before_close', self) -@log_call -def set_context(ctx): - '''Manually set the current context to the specified :class:`Context`''' - ensure_instance(ctx, Context) + _log.debug('Unloading path...') + self.path.unload() - global _context - _context = ctx + _log.debug('Unloading UI...') + self.ui.unload() + _log.debug('Unloading IO...') + self.io.unload() -@log_call -def set_context_from_entry(entry): - '''Extract and set the current context from the specified entry''' + _log.debug('Unloading settings...') + self.settings.unload() - global _context - new_context = Context.from_path(entry.path) - _context.update(new_context, exclude=['host', 'root']) + _log.debug('Unloading context...') + self.context.unload() + _log.debug('Unloading extensions...') + self.extensions.unload() -@log_call -def set_context_from_path(path): - '''Extract and set the current context from the specified path''' + self.events.send('after_close', self) - global _context - new_context = Context.from_path(path) - _context.update(new_context, exclude=['host', 'root']) + _log.debug('Unloading events...') + self.events.unload() + _cache.pop(self.name, None) + self.initialized = False + _log.debug('Done uninitializing.') + _log.debug('Goodbye!') -@log_call -def get_context(): - '''Get current :class:`Context`''' + def get_context(self): + '''Get a copy of the current context. - return _ctx_stack.top or _context + .. seealso:: :class:`construct.context.Context` + ''' + return self.context.copy() -@log_call -def get_request(): - '''Get current task :class:`Request`''' + def set_context(self, ctx): + '''Set the current context. - return _req_stack.top + Examples: + Set the active context permanently:: + >>> new_ctx = api.get_context() + >>> new_ctx.project = 'A_PROJECT' + >>> api.set_context(new_context) -@log_call -def get_host(name=None): - '''Get current host extension or get host by name''' + Temporarily set context:: - ctx = get_context() - host = name or ctx.host - return extensions.get(host, None) + >>> with api.set_context(new_ctx): + ... # Do something while ctx is new_ctx + Arguments: + ctx (Context) - Sets api. + ''' -@log_call -def get_form(action_identifier): - '''Get form for action_identifier''' + _api = self + _ctx = self.context + self.context = ctx - for ext in extensions: - form = ext.get_form(action_identifier) - if form: - return form + class _AsContextManager(object): + def __enter__(self): + pass -@log_call -def get_path_template(name): - '''Get one of the current projects templates by name''' + def __exit__(self, *args): + _api.context = _ctx - return lucidity.Template(name, config['PATH_TEMPLATES'][name], anchor=None) + return _AsContextManager() + def update_context(self, **data): + '''Update the current context. -@log_call -def get_path_templates(): - '''Get a dict containining the current projects templates''' + Arguments: + **data - Context keys and values. + ''' - return {k: lucidity.Template(k, v, anchor=None) - for k, v in config['PATH_TEMPLATES']} + self.context.update(data) + def set_context_from_path(self, path): + '''Set the current api context using a file or directory path.''' -@log_call -def get_template_search_paths(): + self.context = self.context_from_path(path) - ctx = get_context() + def context_from_path(self, path): - paths = set() - if ctx.project: - paths.add(unipath(ctx.project.data.path, 'templates')) + ctx = Context(host=self.context.host) - for ext in extensions: - paths.update(ext._template_paths) + path = unipath(path) + if path.is_file(): + ctx.file = path.as_posix() - return [p for p in paths if os.path.isdir(p)] + location_mount = self.get_mount_from_path(path) + if location_mount: + ctx.update(location=location_mount[0], mount=location_mount[1]) + else: + raise ContextError( + 'Failed to extract context from path. ' + 'Path does not appear to match your configured locations.' + ) + + # TODO: Extract remaining context from path + + return ctx + + def set_mount(self, location, mount): + self.update_context(location=location, mount=mount) + + def get_mount(self, location=None, mount=None): + '''Get a file system path to a mount. Uses the current context location + and mount if they are not provided. + + Arguments: + location (str): Name of location or context['location'] + mount (str): Name of mount or context['mount'] + ''' + + location = location or self.context.location + mount = mount or self.context.mount + path = self.settings['locations'][location][mount] + if isinstance(path, dict): + path = path[self.context.platform] + ensure_exists(path) + return unipath(path) + else: + ensure_exists(path) + return unipath(path) + def get_mount_from_path(self, path): + '''Get the location and mount from a file path''' -@log_call -def get_template(name, *tags): - '''Get a template by tag and name''' + path = unipath(path) - templates = get_templates(*tags) + for location, mounts in self.settings['locations'].items(): + for mount, mount_path in mounts.items(): + if unipath(mount_path) in path.parents: + return location, mount - alt = None - for template in templates.values(): - if name == template.name: - return template - if name in template.name: - alt = template.name + def get_locations(self): + '''Get locations from settings.''' - if alt: - msg = '"{}" not found, did you mean "{}"?'.format(name, alt) - else: - msg = '"{}" not found'.format(name) - raise TemplateError(msg) + return self.settings['locations'] + @property + def host(self): + '''Get the active Host Extension.''' -@log_call -def get_templates(*tags): - '''Get all templates matching a set of tags.''' + return self.extensions.get(self.context.host, None) - entries = chain(*( - fsfs.search(path, depth=1).tags(*tags) - for path in get_template_search_paths() - )) + def define(self, event, doc): + '''Define a new event - templates = {} - for template in entries: - if template.name in templates: - continue - templates[template.name] = template + Arguments: + event (str): Name of the event + doc (str): Documentation for event + ''' - return templates + self.events.define(event, doc) + def undefine(self, event): + '''Undefine an event -@log_call -def search(name=None, tags=None, **kwargs): - '''Search for Construct Entries by name or tag''' + Arguments: + event (str): Name of the event + ''' - ctx = get_context() - root = kwargs.pop('root', None) + self.events.undefine(event) - if root is None: - entry = ctx.get_deepest_entry() - if entry: - root = entry.path - else: - root = ctx.root or os.getcwd() + def on(self, *args): + '''Adds a handler to the specified event. Can be used as a decorator. + + Examples: + >>> api.on('greet', lambda person: print('Hello %s' % person)) + >>> @api.on('greet') + ... def greeter(person): + ... print('Hello %s' % person) + + Arguments: + event (str): Name of event + handler (callable): Function to add as handler + priority (int): Priority of handler + + Decorator Arguments: + event (str): Name of event + priority (int): Priority of handler + ''' + + self.events.on(*args) - entries = fsfs.search(root, **kwargs) - if name: - entries = entries.name(name) - if tags: - tags = fsfs.util.tupilize(tags) - entries = entries.tags(*tags) - return entries + def off(self, event, handler): + '''Remove a handler from an event. + Arguments: + event (str): Name of the event + handler (callable): Handler to remove + ''' -@log_call -def quick_select(selector, **kwargs): - '''Quickly find one Entry using a selector string like entry1/entry2''' + self.events.off(event, handler) - ctx = get_context() - root = kwargs.pop('root', None) + def send(self, event, *args, **kwargs): + '''Send an event. Executes all the event handlers and returns a + list of the handlers results. - if root is None: - entry = ctx.get_deepest_entry() - if entry: - root = entry.path + Arguments: + event (str): Name of event to send + *args: Event arguments + **kwargs: Event keyword arguments + ''' + + return self.events.send(event, *args, **kwargs) + + def extend(self, name, member): + '''Register an obj with the API at the specified name. This provides + a way for Extensions to register objects to be first-class members of + the API. + + Examples: + >>> def say(message): + ... return message + >>> api.extend('say', say) + >>> api.say('Hello world!') + 'Hello world! + ''' + + if hasattr(self, name): + raise NameError('API already has a member named "%s".' % name) + + if requires_wrapper(member): + member = api_method_wrapper(self, member) + + _log.debug('Adding %s to api.' % name) + self._registered_members[name] = member + setattr(self, name, member) + + def unextend(self, name): + '''Unregister a name that was registered using extend. + + Examples: + >>> api.unextend('say') + ''' + + if name in self._registered_members: + _log.debug('Removing %s from api.' % name) + self._registered_members.pop(name, None) + self.__dict__.pop(name, None) else: - root = ctx.root or os.getcwd() + _log.debug(name + ' was not registered with api.extend.') + + def show(self, data): + '''Pretty print a dict or list of dicts.''' + + if isinstance(data, Mapping): + print(yaml_dump(dict(data)).decode('utf-8')) + return + elif isinstance(data, basestring): + print(data) + return + + try: + for obj in data: + print('') + print(yaml_dump(obj).decode('utf-8')) + except: + print('Can not format: %s' % data) + - first_depth = 5 if ctx.project else 3 - if ctx.project: - first_depth = 4 +def api_method_wrapper(api, fn): - return fsfs.quick_select(root, selector, first_depth=first_depth) + @wraps(fn) + def api_method_call(*args, **kwargs): + return fn(api, *args, **kwargs) + return api_method_call -# Builtin Action Aliases -new_project = ActionProxy('new.project') -new_sequence = ActionProxy('new.sequence') -new_shot = ActionProxy('new.shot') -new_asset_type = ActionProxy('new.asset_type') -new_asset = ActionProxy('new.asset') -new_task = ActionProxy('new.task') -new_workspace = ActionProxy('new.workspace') -new_template = ActionProxy('new.template') -save = ActionProxy('save') -publish = ActionProxy('publish') -publish_file = ActionProxy('publish_file') +def requires_wrapper(obj): + return callable(obj) and 'api' in inspect.getargspec(obj)[0] diff --git a/construct/builtins/__init__.py b/construct/builtins/__init__.py deleted file mode 100644 index af19932..0000000 --- a/construct/builtins/__init__.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from construct.extension import Extension -from construct.utils import package_path -from construct.builtins import ( - assets, - collections, - files, - projects, - sequences, - shots, - tasks, - templates, - workspaces, - time -) - - -class Builtins(Extension): - '''Builtin Actions and Tasks''' - - name = 'Builtins' - attr_name = 'builtins' - - def available(self, ctx): - return True - - def load(self): - - self.add_template_path(package_path('builtins/templates')) - - self.add_action(projects.NewProject) - self.add_task(projects.NewProject, projects.stage_project) - self.add_task(projects.NewProject, projects.validate_project) - self.add_task(projects.NewProject, projects.commit_project) - - self.add_action(collections.NewCollection) - self.add_task(collections.NewCollection, collections.stage_collection) - self.add_task(collections.NewCollection, collections.validate_collection) - self.add_task(collections.NewCollection, collections.commit_collection) - - self.add_action(sequences.NewSequence) - self.add_task(sequences.NewSequence, sequences.stage_sequence) - self.add_task(sequences.NewSequence, sequences.validate_sequence) - self.add_task(sequences.NewSequence, sequences.commit_sequence) - - self.add_action(shots.NewShot) - self.add_task(shots.NewShot, shots.stage_shot) - self.add_task(shots.NewShot, shots.validate_shot) - self.add_task(shots.NewShot, shots.commit_shot) - - self.add_action(assets.NewAsset) - self.add_task(assets.NewAsset, assets.stage_asset) - self.add_task(assets.NewAsset, assets.validate_asset) - self.add_task(assets.NewAsset, assets.commit_asset) - - self.add_action(assets.NewAssetType) - self.add_task(assets.NewAssetType, assets.stage_asset_type) - self.add_task(assets.NewAssetType, assets.validate_asset_type) - self.add_task(assets.NewAssetType, assets.commit_asset_type) - - self.add_action(tasks.NewTask) - self.add_task(tasks.NewTask, tasks.stage_task) - self.add_task(tasks.NewTask, tasks.validate_task) - self.add_task(tasks.NewTask, tasks.commit_task) - - self.add_action(workspaces.NewWorkspace) - self.add_task(workspaces.NewWorkspace, workspaces.stage_workspace) - self.add_task(workspaces.NewWorkspace, workspaces.validate_workspace) - self.add_task(workspaces.NewWorkspace, workspaces.commit_workspace) - - self.add_action(templates.NewTemplate) - self.add_task(templates.NewTemplate, templates.stage_template) - self.add_task(templates.NewTemplate, templates.validate_template) - self.add_task(templates.NewTemplate, templates.commit_template) - - self.add_action(files.Publish) - self.add_action(files.PublishFile) - - self.add_action(files.Open) - self.add_task(files.Open, files.open_file) - - self.add_action(files.Save) - self.add_task(files.Save, files.build_filename) - self.add_task(files.Save, files.save_file) - - self.add_action(time.SaveFrameRange) - self.add_task(time.SaveFrameRange, time.store_frame_range) - - self.add_action(time.SyncFrameRange) - self.add_task(time.SyncFrameRange, time.get_frame_range) - self.add_task(time.SyncFrameRange, time.apply_frame_range) - - self.add_action(time.SaveFPS) - self.add_task(time.SaveFPS, time.store_frame_rate) - - self.add_action(time.SyncFPS) - self.add_task(time.SyncFPS, time.get_frame_rate) - self.add_task(time.SyncFPS, time.apply_frame_rate) diff --git a/construct/builtins/assets.py b/construct/builtins/assets.py deleted file mode 100644 index dfcff71..0000000 --- a/construct/builtins/assets.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct import api -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct.errors import Abort -from construct import types -import fsfs - - -class NewAssetType(Action): - '''Create a new Asset Type''' - - label = 'New Asset Type' - identifier = 'new.asset_type' - returns = artifact('asset_type') - - @staticmethod - def parameters(ctx): - params = dict( - project={ - 'label': 'Project', - 'help': 'Project Entry', - 'required': True, - 'type': types.Entry, - }, - collection={ - 'label': 'Collection', - 'help': 'Collection Name', - 'required': True, - 'type': types.String - }, - name={ - 'label': 'Asset Type Name', - 'required': True, - 'type': types.String, - 'help': 'Name of Asset Type', - }, - ) - - if not ctx: - return params - - if ctx.project: - params['project']['default'] = ctx.project - params['project']['required'] = False - - if ctx.collection: - params['collection']['default'] = ctx.collection.name - params['collection']['required'] = False - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - ctx.collection and - not ctx.asset_type and - not ctx.sequence - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('asset_type')) -def stage_asset_type(project, collection, name): - '''Stage new asset_type Entry''' - - path_template = api.get_path_template('asset_type') - asset_type_path = path_template.format(dict( - project=project.path, - collection=collection, - asset_type=name, - )) - - return fsfs.get_entry(asset_type_path) - - -@task(priority=types.VALIDATE) -@requires(success('stage_asset_type')) -@params(store('asset_type')) -def validate_asset_type(asset_type): - '''Make sure new asset_type does not already exist''' - - if asset_type.exists: - raise Abort('Asset Type already exists: %s' % asset_type.name) - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_asset_type')) -@params(store('asset_type')) -@returns(artifact('asset_type')) -def commit_asset_type(asset_type): - '''Commit new asset_type Entry''' - - asset_type.tag('asset_type') - return asset_type - - -class NewAsset(Action): - '''Create a new Asset''' - - label = 'New Asset' - identifier = 'new.asset' - returns = artifact('asset') - - @staticmethod - def parameters(ctx): - params = dict( - project={ - 'label': 'Project', - 'help': 'Project Entry', - 'required': True, - 'type': types.Entry, - }, - collection={ - 'label': 'Collection', - 'help': 'Collection Name', - 'required': True, - 'type': types.String - }, - asset_type={ - 'label': 'Asset Type', - 'required': True, - 'type': types.String, - 'help': 'Type of asset', - }, - name={ - 'label': 'Asset Name', - 'required': True, - 'type': types.String, - 'help': 'Name of asset' - }, - template={ - 'label': 'Asset Template', - 'required': False, - 'type': types.String, - 'help': 'Name of asset template' - } - ) - - if not ctx: - return params - - if ctx.project: - params['project']['default'] = ctx.project - params['project']['required'] = False - - # TODO: fix search speed upstream... - # collection_types = [e.name for e in ctx.project.collections] - # params['collection']['options'] = collection_types - - # asset_types = [e.name for e in ctx.project.asset_types] - # params['asset_type']['options'] = asset_types - - if ctx.collection: - params['collection']['default'] = ctx.collection.name - params['collection']['required'] = False - - if ctx.asset_type: - params['asset_type']['default'] = ctx.asset_type.name - params['asset_type']['required'] = False - - templates = list(api.get_templates('asset').keys()) - if templates: - params['template']['options'] = templates - params['template']['default'] = sorted(templates)[0] - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - ctx.collection and - ctx.asset_type and - not ctx.asset - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('asset_item')) -def stage_asset(project, collection, asset_type, name, template=None): - '''Stage a new Asset''' - - path_template = api.get_path_template('asset') - asset_path = path_template.format(dict( - project=project.path, - collection=collection, - asset_type=asset_type, - asset=name - )) - - try: - template = api.get_template(template, 'asset') - except KeyError: - template = None - - return dict( - name=name, - path=asset_path, - tags=['asset'], - template=template, - ) - - -@task(priority=types.VALIDATE) -@requires(success('stage_asset')) -@params(store('asset_item')) -def validate_asset(asset_item): - '''Make sure new asset does not exist''' - - if os.path.exists(asset_item['path']): - raise Abort('Asset already exists: ' + asset_item['name']) - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_asset')) -@params(store('asset_item')) -@returns(artifact('asset')) -def commit_asset(asset_item): - '''Make new asset''' - - if asset_item['template']: - asset = asset_item['template'].copy(asset_item['path']) - else: - asset = fsfs.get_entry(asset_item['path']) - - asset.tag(*asset_item['tags']) - - return asset diff --git a/construct/builtins/collections.py b/construct/builtins/collections.py deleted file mode 100644 index 4557a11..0000000 --- a/construct/builtins/collections.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct import api -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct.errors import Abort -from construct import types -import fsfs - - -class NewCollection(Action): - '''Create a new Collection''' - - label = 'New Collection' - identifier = 'new.collection' - returns = artifact('collection') - - @staticmethod - def parameters(ctx): - params = dict( - project={ - 'label': 'Project', - 'help': 'Project Entry', - 'required': True, - 'type': types.Entry, - }, - name={ - 'label': 'Collection Name', - 'required': True, - 'type': types.String, - 'help': 'Name of Collection', - }, - template={ - 'label': 'Collection Template', - 'require': False, - 'type': types.String, - 'help': 'Name of template', - }, - ) - - if not ctx: - return params - - if ctx.project: - params['project']['default'] = ctx.project - params['project']['required'] = False - - templates = list(api.get_templates('collection').keys()) - if templates: - params['template']['options'] = templates - params['template']['default'] = templates[0] - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - not ctx.collection - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('collection_item')) -def stage_collection(project, name, template=None): - '''Stage a new collection''' - - path_template = api.get_path_template('collection') - collection_path = path_template.format(dict( - project=project.path, - collection=name - )) - - try: - template = api.get_template(template, 'collection') - except KeyError: - template = None - - return dict( - name=name, - path=collection_path, - tags=['collection'], - template=template, - ) - - -@task(priority=types.VALIDATE) -@requires(success('stage_collection')) -@params(store('collection_item')) -def validate_collection(collection_item): - '''Make sure new collection does not exist''' - - if os.path.exists(collection_item['path']): - raise Abort('collection already exists: ' + collection_item['name']) - - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_collection')) -@params(store('collection_item')) -@returns(artifact('collection')) -def commit_collection(collection_item): - '''Make new collection''' - - if collection_item['template']: - collection = collection_item['template'].copy(collection_item['path']) - else: - collection = fsfs.get_entry(collection_item['path']) - - collection.tag(*collection_item['tags']) - - return collection diff --git a/construct/builtins/files.py b/construct/builtins/files.py deleted file mode 100644 index 70757d2..0000000 --- a/construct/builtins/files.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct import types, get_host, utils, get_path_template -from construct.vendor.lucidity.error import ParseError - - -class Publish(Action): - '''Publish the current open file''' - - label = 'Publish' - identifier = 'publish' - returns = artifact('file') - - @staticmethod - def parameters(ctx): - params = dict( - task={ - 'label': 'Task', - 'required': True, - 'type': types.Entry, - 'help': 'Task Entry', - }, - version={ - 'label': 'Version', - 'required': True, - 'type': types.Integer, - 'help': 'File Version' - }, - name={ - 'label': 'Name', - 'required': True, - 'type': types.String, - 'help': 'Publish Name' - } - ) - - # TODO: Implement contextual defaults and options - return params - - @staticmethod - def available(ctx): - return ctx.host != 'cli' and ctx.task - - -class PublishFile(Action): - '''Publish a file to the specified task''' - - label = 'Publish File' - identifier = 'publish.file' - returns = artifact('file') - - @staticmethod - def parameters(ctx): - params = dict( - task={ - 'label': 'Task', - 'required': True, - 'type': types.Entry, - 'help': 'Task', - }, - version={ - 'label': 'Version', - 'required': True, - 'type': types.Integer, - 'help': 'File Version' - }, - file={ - 'label': 'File path', - 'required': True, - 'type': types.String, - 'help': 'Path to file you want to publish' - } - ) - - # TODO: Implement contextual defaults and options - return params - - @staticmethod - def available(ctx): - return ctx.host == 'cli' and ctx.task - - -class Save(Action): - '''Save the current file''' - - label = 'Save' - identifier = 'file.save' - returns = artifact('file') - - @staticmethod - def parameters(ctx): - params = dict( - workspace={ - 'label': 'Workspace', - 'required': False, - 'type': types.Entry, - 'help': 'Workspace to save to' - }, - name={ - 'label': 'Name', - 'required': False, - 'type': types.String, - 'help': 'Filename', - }, - version={ - 'label': 'Version', - 'required': True, - 'type': types.Integer, - 'help': 'File Version' - }, - ext={ - 'label': 'Extension', - 'require': True, - 'type': types.String, - 'help': 'File Extension' - } - ) - - if not ctx: - return params - - params['workspace']['default'] = ctx.workspace - name = ctx.task.parent().name - if ctx.file: - path_template = get_path_template('workspace_file') - try: - data = path_template.parse(str(ctx.file)) - except ParseError: - pass - else: - name = data['name'] - - params['name']['default'] = name - - extensions = ctx.workspace.config['extensions'] - params['ext']['default'] = extensions[0] - params['ext']['options'] = extensions - params['ext']['required'] = False - next_version = ctx.workspace.get_next_version(name, extensions[0]) - params['version']['default'] = next_version - params['version']['required'] = False - return params - - @staticmethod - def available(ctx): - return ctx.host != 'cli' and ctx.workspace - - -@task -@pass_kwargs -@returns(store('file')) -def build_filename(workspace, name, version, ext): - '''Builds the full save path''' - - task = workspace.parent() - path_template = get_path_template('workspace_file') - filename = path_template.format(dict( - task=task.short, - name=name, - version='{:0>3d}'.format(version), - ext=ext - )) - return utils.unipath(workspace.path, filename) - - -@task -@requires(success('build_filename')) -@params(store('file')) -def save_file(file): - '''Save file in Host application''' - - host = get_host() - host.save_file(file) - - -class Open(Action): - '''Open a file''' - - label = 'Open' - identifier = 'file.open' - parameters = dict( - file={ - 'label': 'File path', - 'required': True, - 'type': types.String, - 'help': 'Path to file you want to publish' - } - ) - - @staticmethod - def available(ctx): - return ctx.host != 'cli' and ctx.task - - -@task -@pass_kwargs -def open_file(file): - '''Open file in Host application''' - - host = get_host() - host.open_file(file) diff --git a/construct/builtins/projects.py b/construct/builtins/projects.py deleted file mode 100644 index 8d97743..0000000 --- a/construct/builtins/projects.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct import api, types -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct.utils import unipath -from construct.errors import Abort - - -class NewProject(Action): - '''Create a new Project''' - - label = 'New Project' - identifier = 'new.project' - returns = artifact('project') - - @classmethod - def parameters(cls, ctx): - params = dict( - root={ - 'label': 'Project Root', - 'required': True, - 'type': types.String, - 'help': 'project root directory', - }, - template={ - 'label': 'Project Template', - 'required': True, - 'type': types.String, - 'help': 'name of a project template', - } - ) - - if not ctx: - return params - - templates = list(api.get_templates('project').keys()) - params['template']['options'] = templates - if templates: - params['template']['default'] = templates[0] - - return params - - @staticmethod - def available(ctx): - return not ctx.project - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('project_item')) -def stage_project(root, template): - '''Stage project data for validation''' - - return dict( - path=unipath(root), - name=os.path.basename(root), - template=api.get_template(template) - ) - - -@task(priority=types.VALIDATE) -@params(store('project_item')) -@requires(success('stage_project')) -def validate_project(project_item): - '''Make sure the project does not already exist.''' - - if os.path.exists(project_item['path']): - raise Abort('Project already exists: %s' % project_item['path']) - - return True - - -@task(priority=types.COMMIT) -@params(store('project_item')) -@requires(success('validate_project')) -@returns(artifact('project')) -def commit_project(project_item): - '''Copy the project template to project directory''' - - project = project_item['template'].copy(project_item['path']) - return project diff --git a/construct/builtins/sequences.py b/construct/builtins/sequences.py deleted file mode 100644 index 34bf89e..0000000 --- a/construct/builtins/sequences.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct import api -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct import types -from construct.errors import Abort -import fsfs - - -class NewSequence(Action): - '''Create a new Sequence''' - - label = 'New Sequence' - identifier = 'new.sequence' - returns = artifact('sequence') - - @staticmethod - def parameters(ctx): - params = dict( - project={ - 'label': 'Project', - 'help': 'Project Entry', - 'required': True, - 'type': types.Entry, - }, - collection={ - 'label': 'Collection', - 'help': 'Collection Name', - 'required': True, - 'type': types.String - }, - name={ - 'label': 'Sequence Name', - 'required': True, - 'type': types.String, - 'help': 'Name of sequence', - }, - template={ - 'label': 'Sequence Template', - 'required': False, - 'type': types.String, - 'help': 'Name of sequence template' - } - ) - - if not ctx: - return params - - if ctx.project: - params['project']['default'] = ctx.project - params['project']['required'] = False - - # TODO: fix search speed upstream... - # collection_types = [e.name for e in ctx.project.collections] - # params['collection']['options'] = collection_types - - if ctx.collection: - params['collection']['default'] = ctx.collection.name - params['collection']['required'] = False - - templates = list(api.get_templates('sequence').keys()) - if templates: - params['template']['default'] = templates[0] - params['template']['options'] = templates - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - ctx.collection and - not ctx.sequence and - not ctx.asset_type - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('sequence_item')) -def stage_sequence(project, collection, name, template=None): - - path_template = api.get_path_template('sequence') - sequence_path = path_template.format(dict( - project=project.path, - collection=collection, - sequence=name - )) - - try: - template = api.get_template(template, 'sequence') - except KeyError: - template = None - - return dict( - name=name, - path=sequence_path, - tags=['sequence'], - template=template, - ) - - -@task(priority=types.VALIDATE) -@requires(success('stage_sequence')) -@params(store('sequence_item')) -def validate_sequence(sequence_item): - if os.path.exists(sequence_item['path']): - raise Abort('Sequence already exists: ' + sequence_item['name']) - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_sequence')) -@params(store('sequence_item')) -@returns(artifact('sequence')) -def commit_sequence(sequence_item): - '''Make new task''' - - if sequence_item['template']: - sequence = sequence_item['template'].copy(sequence_item['path']) - else: - sequence = fsfs.get_entry(sequence_item['path']) - - sequence.tag(*sequence_item['tags']) - - return sequence diff --git a/construct/builtins/shots.py b/construct/builtins/shots.py deleted file mode 100644 index b50c63b..0000000 --- a/construct/builtins/shots.py +++ /dev/null @@ -1,148 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct import api -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct import types -from construct.errors import Abort -import fsfs - - -class NewShot(Action): - '''Create a new Shot''' - - label = 'New Shot' - identifier = 'new.shot' - returns = artifact('shot') - - @staticmethod - def parameters(ctx): - params = dict( - project={ - 'label': 'Project', - 'help': 'Project Entry', - 'required': True, - 'type': types.Entry, - }, - collection={ - 'label': 'Collection', - 'help': 'Collection Name', - 'required': True, - 'type': types.String - }, - sequence={ - 'label': 'Sequence', - 'required': True, - 'type': types.Entry, - 'help': 'Name of sequence', - }, - name={ - 'label': 'Shot Name', - 'required': True, - 'type': types.String, - 'help': 'Name of shot' - }, - template={ - 'label': 'Shot Template', - 'required': False, - 'type': types.String, - 'help': 'Name of shot template' - } - ) - - if not ctx: - return params - - if ctx.project: - params['project']['default'] = ctx.project - params['project']['required'] = False - - # TODO: fix search speed upstream... - # collection_types = [e.name for e in ctx.project.collections] - # params['collection']['options'] = collection_types - - if ctx.collection: - params['collection']['default'] = ctx.collection.name - params['collection']['required'] = False - - if ctx.sequence: - params['sequence']['default'] = ctx.sequence - params['sequence']['required'] = False - - templates = list(api.get_templates('shot').keys()) - if templates: - params['template']['options'] = templates - params['template']['default'] = sorted(templates)[0] - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - ctx.collection and - ctx.sequence and - not ctx.shot - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('shot_item')) -def stage_shot(project, collection, sequence, name, template=None): - - path_template = api.get_path_template('shot') - shot_path = path_template.format(dict( - project=project.path, - collection=collection, - sequence=sequence.name, - shot=name - )) - - try: - template = api.get_template(template, 'shot') - except KeyError: - template = None - - return dict( - name=name, - path=shot_path, - tags=['shot'], - template=template, - ) - - -@task(priority=types.VALIDATE) -@requires(success('stage_shot')) -@params(store('shot_item')) -def validate_shot(shot_item): - if os.path.exists(shot_item['path']): - raise Abort('Shot already exists: ' + shot_item['name']) - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_shot')) -@params(store('shot_item')) -@returns(artifact('shot')) -def commit_shot(shot_item): - '''Make new task''' - - if shot_item['template']: - shot = shot_item['template'].copy(shot_item['path']) - else: - shot = fsfs.get_entry(shot_item['path']) - - shot.tag(*shot_item['tags']) - - return shot diff --git a/construct/builtins/tasks.py b/construct/builtins/tasks.py deleted file mode 100644 index 7376b5c..0000000 --- a/construct/builtins/tasks.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct import api, config, types -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct.utils import unipath -from construct.errors import Abort -import fsfs - - -class NewTask(Action): - '''Create a new Task''' - - label = 'New Task' - identifier = 'new.task' - returns = artifact('task') - - @staticmethod - def parameters(ctx): - params = dict( - parent={ - 'label': 'Parent Entry', - 'required': True, - 'type': types.Entry, - 'help': 'Parent entry of task', - }, - type={ - 'label': 'Task Type', - 'required': True, - 'type': types.String, - 'help': 'Type of task', - }, - name={ - 'label': 'Task Name', - 'required': False, - 'type': types.String, - 'help': 'Name of task' - }, - template={ - 'label': 'Task Template Name', - 'required': False, - 'type': types.String, - 'help': 'Name of task template' - } - ) - - if not ctx: - return params - - params['type']['options'] = list(config['TASK_TYPES'].keys()) - - if ctx.shot: - params['parent']['default'] = ctx.shot - params['parent']['required'] = False - - elif ctx.asset: - params['parent']['default'] = ctx.asset - params['parent']['required'] = False - - templates = list(api.get_templates('task').keys()) - if templates: - params['template']['options'] = templates - params['template']['default'] = templates[0] - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - (ctx.shot or ctx.asset) and - not ctx.task - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('task_item')) -def stage_task(parent, type, name=None, template=None): - '''Stage task for creation''' - - name = name or type - return dict( - name=name, - path=unipath(parent.path, name), - tags=['task', type], - template=api.get_template(template, 'task'), - ) - - -@task(priority=types.VALIDATE) -@requires(success('stage_task')) -@params(store('task_item')) -def validate_task(task_item): - '''Validate potential task''' - - if os.path.exists(task_item['path']): - raise Abort('Task already exists: ' + task_item['name']) - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_task')) -@params(store('task_item')) -@returns(artifact('task')) -def commit_task(task_item): - '''Create new task''' - - if task_item['template']: - task = task_item['template'].copy(task_item['path']) - else: - task = fsfs.get_entry(task_item['path']) - - task.tag(*task_item['tags']) - - return task diff --git a/construct/builtins/templates.py b/construct/builtins/templates.py deleted file mode 100644 index d5ee2f2..0000000 --- a/construct/builtins/templates.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -import construct -from construct.action import Action -from construct.errors import Abort -from construct.tasks import ( - task, - pass_kwargs, - requires, - success, - pass_context, - store, - kwarg, - artifact, - params, - returns -) -from construct import types -from construct.utils import unipath - - -class NewTemplate(Action): - '''Create a new Template from an Entry''' - - label = 'New Template' - identifier = 'new.template' - returns = artifact('template') - - @staticmethod - def parameters(ctx): - params = dict( - name={ - 'label': 'Template Name', - 'required': True, - 'type': types.String, - 'help': 'Name of new template', - }, - entry={ - 'label': 'Entry', - 'required': True, - 'type': types.Entry, - 'help': 'Entry to create template from', - }, - include_files={ - 'label': 'Include Files', - 'type': bool, - 'default': False, - 'help': 'Include all files in entry not just entry data', - } - ) - if not ctx: - return params - - if ctx.host == 'cli': - params['entry']['default'] = ctx.get_deepest_entry() - - return params - - @staticmethod - def available(ctx): - return False - - -@task(priority=types.STAGE) -@pass_context -@pass_kwargs -@returns(store('template')) -def stage_template(ctx, name, entry, include_files): - '''Stage template data''' - - templates_path = unipath(ctx.project.data.path, 'templates') - template_path = unipath(templates_path, name) - - template_item = types.Namespace( - entry=entry, - name=name, - path=template_path, - include_files=include_files, - ) - return template_item - - -@task(priority=types.VALIDATE) -@requires(success('stage_template_data')) -@params(store('template')) -def validate_template(template): - '''Make sure template does not already exist''' - - if ( - os.path.exists(template.path) or - template.name in construct.get_templates() - ): - raise Abort('Template already exists: ' + template.name) - - -@task(priority=types.COMMIT) -@requires(success('validate_template')) -@params(store('template')) -@returns(artifact('template')) -def commit_template(template): - '''Commit template''' - - new_template = entry.copy(template_path, only_data=not include_files) - return new_template diff --git a/construct/builtins/templates/vfx_asset/.data/data b/construct/builtins/templates/vfx_asset/.data/data deleted file mode 100644 index db8464e..0000000 --- a/construct/builtins/templates/vfx_asset/.data/data +++ /dev/null @@ -1 +0,0 @@ -status: waiting diff --git a/construct/builtins/templates/vfx_project/.data/data b/construct/builtins/templates/vfx_project/.data/data deleted file mode 100644 index dae77cb..0000000 --- a/construct/builtins/templates/vfx_project/.data/data +++ /dev/null @@ -1,2 +0,0 @@ -status: in progress -fps: 24fps diff --git a/construct/builtins/templates/vfx_project/assets/prop/.data/uuid_7cb5a9c8-4aaf-4ee4-a90b-61805478fc13 b/construct/builtins/templates/vfx_project/assets/prop/.data/uuid_7cb5a9c8-4aaf-4ee4-a90b-61805478fc13 deleted file mode 100644 index e69de29..0000000 diff --git a/construct/builtins/templates/vfx_sequence/.data/data b/construct/builtins/templates/vfx_sequence/.data/data deleted file mode 100644 index db8464e..0000000 --- a/construct/builtins/templates/vfx_sequence/.data/data +++ /dev/null @@ -1 +0,0 @@ -status: waiting diff --git a/construct/builtins/templates/vfx_sequence/.data/tag_sequence b/construct/builtins/templates/vfx_sequence/.data/tag_sequence deleted file mode 100644 index e69de29..0000000 diff --git a/construct/builtins/templates/vfx_shot/.data/data b/construct/builtins/templates/vfx_shot/.data/data deleted file mode 100644 index db8464e..0000000 --- a/construct/builtins/templates/vfx_shot/.data/data +++ /dev/null @@ -1 +0,0 @@ -status: waiting diff --git a/construct/builtins/templates/vfx_shot/.data/tag_shot b/construct/builtins/templates/vfx_shot/.data/tag_shot deleted file mode 100644 index e69de29..0000000 diff --git a/construct/builtins/templates/vfx_task/.data/data b/construct/builtins/templates/vfx_task/.data/data deleted file mode 100644 index db8464e..0000000 --- a/construct/builtins/templates/vfx_task/.data/data +++ /dev/null @@ -1 +0,0 @@ -status: waiting diff --git a/construct/builtins/templates/vfx_task/.data/tag_task b/construct/builtins/templates/vfx_task/.data/tag_task deleted file mode 100644 index e69de29..0000000 diff --git a/construct/builtins/templates/vfx_workspace/.data/data b/construct/builtins/templates/vfx_workspace/.data/data deleted file mode 100644 index db8464e..0000000 --- a/construct/builtins/templates/vfx_workspace/.data/data +++ /dev/null @@ -1 +0,0 @@ -status: waiting diff --git a/construct/builtins/templates/vfx_workspace/.data/tag_workspace b/construct/builtins/templates/vfx_workspace/.data/tag_workspace deleted file mode 100644 index e69de29..0000000 diff --git a/construct/builtins/time.py b/construct/builtins/time.py deleted file mode 100644 index 35c2fa2..0000000 --- a/construct/builtins/time.py +++ /dev/null @@ -1,251 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from construct.action import Action -from construct.tasks import ( - task, - pass_context, - pass_kwargs, - returns, - artifact, - store, - params, - success, - requires -) -from construct import types, get_host, utils, get_path_template -from construct.vendor.lucidity.error import ParseError - - -class SaveFrameRange(Action): - '''Save the frame range for this shot or asset''' - - label = 'Save Frame Range' - identifier = 'time.saveframerange' - - @staticmethod - def parameters(ctx): - params = dict( - asset_or_shot={ - 'label': 'Asset or Shot', - 'required': True, - 'type': types.Entry, - 'help': 'Asset or Shot Entry' - }, - min={ - 'label': 'Min Frame', - 'required': True, - 'type': types.Number, - 'help': 'Min Frame' - }, - start={ - 'label': 'Start Frame', - 'required': False, - 'type': types.Number, - 'help': 'Animation Start Frame', - }, - end={ - 'label': 'End Frame', - 'required': False, - 'type': types.Number, - 'help': 'Animation End Frame', - }, - max={ - 'label': 'Max Frame', - 'required': True, - 'type': types.Number, - 'help': 'Max Frame' - }, - ) - - if not ctx: - return params - - params['asset_or_shot']['default'] = ctx.asset or ctx.shot - - host = get_host() - if host: - frame_range = host.get_frame_range() - - if frame_range is NotImplemented: - return params - - params['min']['default'] = float(frame_range[0]) - params['start']['default'] = float(frame_range[1]) - params['end']['default'] = float(frame_range[2]) - params['max']['default'] = float(frame_range[3]) - - return params - - @staticmethod - def available(ctx): - return ctx.host and ctx.task - - -class SyncFrameRange(Action): - '''Sync your scenes frame range''' - - label = 'Sync Frame Range' - identifier = 'time.syncframerange' - - @staticmethod - def parameters(ctx): - params = dict( - asset_or_shot={ - 'label': 'Asset or Shot', - 'required': True, - 'type': types.Entry, - 'help': 'Asset or Shot Entry' - } - ) - - if not ctx: - return params - - params['asset_or_shot']['default'] = ctx.asset or ctx.shot - return params - - @staticmethod - def available(ctx): - return ctx.host != 'cli' and ctx.task - - -@task -@pass_kwargs -def store_frame_range(asset_or_shot, min, start, end, max): - '''Stores the frame range in the asset or shot''' - - asset_or_shot.write( - frame_range=[min, start, end, max] - ) - - -@task -@pass_kwargs -@pass_context -@returns(store('frame_range')) -def get_frame_range(ctx, asset_or_shot): - '''Get the Asset or Shot frame range''' - - frame_range = asset_or_shot.data.get( - 'frame_range', - ctx.project.data.get( - 'frame_range', - [101, 101, 580, 580] - ) - ) - return frame_range - - -@task -@requires(store('frame_range')) -@params(store('frame_range')) -@returns(artifact('frame_range')) -def apply_frame_range(frame_range): - '''Applies the frame range to your current scene''' - - host = get_host() - host.set_frame_range(*frame_range) - return {'frame_range': frame_range} - - -class SaveFPS(Action): - '''Save the frame rate for this shot or asset''' - - label = 'Save FPS' - identifier = 'time.savefps' - - @staticmethod - def parameters(ctx): - params = dict( - asset_or_shot={ - 'label': 'Asset or Shot', - 'required': True, - 'type': types.Entry, - 'help': 'Asset or Shot Entry' - }, - fps={ - 'label': 'Frame Rate', - 'required': True, - 'type': types.Number, - 'help': 'Frames per second' - } - ) - - if not ctx: - return params - - params['asset_or_shot']['default'] = ctx.asset or ctx.shot - - host = get_host() - if host: - frame_rate = host.get_frame_rate() - - if frame_rate is NotImplemented: - return params - - params['fps']['default'] = float(frame_rate) - - return params - - @staticmethod - def available(ctx): - return ctx.host and ctx.task - - -class SyncFPS(Action): - '''Sync your scenes frame rate''' - - label = 'Sync FPS' - identifier = 'time.syncfps' - - @staticmethod - def parameters(ctx): - params = dict( - asset_or_shot={ - 'label': 'Asset or Shot', - 'required': True, - 'type': types.Entry, - 'help': 'Asset or Shot Entry' - } - ) - - if not ctx: - return params - - params['asset_or_shot']['default'] = ctx.asset or ctx.shot - return params - - @staticmethod - def available(ctx): - return ctx.host != 'cli' and ctx.task - - -@task -@pass_kwargs -def store_frame_rate(asset_or_shot, fps): - '''Stores the frame rate in the asset or shot''' - - asset_or_shot.write(fps=fps) - - -@task -@pass_kwargs -@pass_context -@returns(store('fps')) -def get_frame_rate(ctx, asset_or_shot): - '''Get the Asset or Shot frame rate''' - - fps = asset_or_shot.data.get('fps', ctx.project.data.get('fps', 24)) - return fps - - -@task -@requires(store('fps')) -@params(store('fps')) -@returns(artifact('fps')) -def apply_frame_rate(fps): - '''Applies the frame rate to your current scene''' - - host = get_host() - host.set_frame_rate(fps) - return {'fps': fps} diff --git a/construct/builtins/workspaces.py b/construct/builtins/workspaces.py deleted file mode 100644 index fd4f2cc..0000000 --- a/construct/builtins/workspaces.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from construct.action import Action -from construct.tasks import ( - task, - pass_kwargs, - requires, - success, - pass_context, - store, - kwarg, - artifact, - returns, - params -) -from construct import types, api -from construct.errors import Abort -import fsfs - - -class NewWorkspace(Action): - '''Create a new Workspace''' - - label = 'New Workspace' - identifier = 'new.workspace' - returns = artifact('workspace') - - @staticmethod - def parameters(ctx): - params = dict( - task={ - 'label': 'Parent Entry', - 'required': True, - 'type': types.Entry, - 'help': 'Parent entry of workspace', - }, - name={ - 'label': 'Workspace Name', - 'required': True, - 'type': types.String, - 'help': 'Name of workspace' - }, - template={ - 'label': 'Workspace Template Name', - 'required': True, - 'type': types.String, - 'help': 'Name of workspace template' - } - ) - - if not ctx: - return params - - if ctx.task: - params['task']['default'] = ctx.task - params['task']['required'] = False - - templates = api.get_templates('workspace') - if templates: - params['template']['options'] = list(templates.keys()) - - return params - - @staticmethod - def available(ctx): - return ( - ctx.project and - (ctx.shot or ctx.asset) and - ctx.task and - not ctx.workspace - ) - - -@task(priority=types.STAGE) -@pass_kwargs -@returns(store('workspace_item')) -def stage_workspace(task, name=None, template=None): - '''Stage workspace for creation''' - - path_template = api.get_path_template('workspace') - path = path_template.format(dict( - task=task.path, - workspace=name - )) - name = name - return dict( - name=name, - path=path, - tags=['workspace'], - template=api.get_template(template, 'workspace'), - ) - - -@task(priority=types.VALIDATE) -@requires(success('stage_workspace')) -@params(store('workspace_item')) -def validate_workspace(workspace_item): - '''Validate potential workspace''' - - if os.path.exists(workspace_item['path']): - raise Abort('Task already exists: ' + workspace_item['name']) - return True - - -@task(priority=types.COMMIT) -@requires(success('validate_workspace')) -@params(store('workspace_item')) -@returns(artifact('workspace')) -def commit_workspace(workspace_item): - '''Create new workspace''' - - if workspace_item['template']: - workspace = workspace_item['template'].copy(workspace_item['path']) - else: - workspace = fsfs.get_entry(workspace_item['path']) - - workspace.tag(*workspace_item['tags']) - - return workspace diff --git a/construct/cli/__init__.py b/construct/cli/__init__.py deleted file mode 100644 index 139759b..0000000 --- a/construct/cli/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import diff --git a/construct/cli/__main__.py b/construct/cli/__main__.py deleted file mode 100644 index 84f445b..0000000 --- a/construct/cli/__main__.py +++ /dev/null @@ -1,248 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function -import os -import sys -import argparse -import colorama -import construct -from collections import OrderedDict -from construct import signals, get_context -from construct.constants import FAILED, SUCCESS, SKIPPED -from construct.cli.commands import commands, ActionCommand -from construct.cli.formatters import ( - Root, - format_section, -) -from construct.cli.utils import styled, style -from construct.cli.constants import ( - OPTIONS_TITLE, - ARTIFACTS_TITLE, - ACTION_CONTEXT_TITLE, - ICONS -) -from construct.cli import stout -from construct.cli.widgets import TaskLine, ProgressBar -import win_unicode_console - - -CONSOLE_WIDGETS = {} - - -@signals.route('action.before') -def on_action_before(ctx): - print() - print(ctx.action.description + '\n') - - if ctx.kwargs or ctx.args: - args_data = [(k, v) for k, v in ctx.kwargs.items()] - if ctx.args: - args_data.append(('extra_args', ctx.args)) - args_section = format_section( - styled('{bright}{fg.yellow}Options{reset}'), - data=args_data, - lcolor=style.bright - ) - print(args_section + '\n') - - ctx_keys = ['action'] + ctx.keys - ctx_data = [] - for k in ctx_keys: - v = ctx[k] - if not v: - continue - if k in ctx.entry_keys: - ctx_data.append((k, v.name)) - else: - ctx_data.append((k, v)) - - ctx_section = format_section( - ACTION_CONTEXT_TITLE, - data=ctx_data, - lcolor=style.bright - ) - print(ctx_section + '\n') - - print('Status Key') - key_tmpl = ' {} {:<8}' - for status, icon in list(ICONS.items())[:5]: - print(key_tmpl.format(icon, status), end='') - print() - for status, icon in list(ICONS.items())[5:]: - print(key_tmpl.format(icon, status), end='') - print('\n') - - console = stout.Console() - console.init() - stout.set_console(console) - - for group, tasks in ctx.task_groups.items(): - print(group.description) - for task in tasks: - w = TaskLine(task, console) - CONSOLE_WIDGETS[task.identifier] = w - console.add_widget(w) - - print() - progress = ProgressBar('Tasks', len(ctx.tasks), console) - CONSOLE_WIDGETS['progress'] = progress - console.add_widget(progress) - - -@signals.route('action.after') -def on_action_after(ctx): - - console = stout.get_console() - console.deinit() - stout.set_console(None) - - # TODO: console deinit turns off colorama and win_unicode_console - # Might want to check colorama and win_unicode_console before - # console.init, if they are already enabled, don't disable them - # in console.deinit, assume someone else is handling it. - win_unicode_console.enable() - colorama.init() - - artifacts = ctx.artifacts.items() - if not artifacts: - print() - return - - data = [(k, v) for k, v in artifacts if v] - if data: - artifacts = format_section( - ARTIFACTS_TITLE, - data=data, - lcolor=style.bright - ) - print('\n' + artifacts) - print() - - -@signals.route('request.status.changed') -def on_request_status_change(request, last_status, status): - - console = stout.get_console() - w = CONSOLE_WIDGETS[request.task.identifier] - w.set_status(status) - - if status == FAILED: - err = w.format_error(request.exception) - console.insert(w.row, err) - - ctx = get_context() - progress = CONSOLE_WIDGETS['progress'] - i = 0 - for request in ctx.requests.values(): - if request._status in [FAILED, SUCCESS, SKIPPED]: - i += 1 - progress.set_value(i) - - -def logging_config(level): - return dict( - version=1, - formatters={ - 'simple': { - 'format': '%(levelname).1s:%(name)s> %(message)s' - } - }, - handlers={ - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'simple', - } - }, - loggers={ - 'construct': { - 'level': level, - 'handlers': ['console'], - 'propagate': False - } - } - ) - - -def setup_parser(): - '''Build the root parser''' - - usage = styled( - '{bright}construct ' - '{fg.blue}{fg.reset} ' - '{fg.yellow}[options]{reset}' - ) - parser = argparse.ArgumentParser( - 'construct', - usage=usage, - formatter_class=Root, - add_help=False, - ) - parser._optionals.title = OPTIONS_TITLE - parser.add_argument( - '-h', - '--help', - help='show this help message and exit', - action='store_true', - dest='-h' - ) - parser.add_argument( - '-v', - '--verbose', - help='verbose output', - action='store_true', - dest='-v' - ) - parser.add_argument( - 'command', - help=argparse.SUPPRESS, - action='store', - nargs='?' - ) - return parser - - -def main(): - '''CLI Entry Point''' - - # Enable ansi console colors - win_unicode_console.enable() - colorama.init() - - parser = setup_parser() - args, extra_args = parser.parse_known_args() - root_flags = [f for f, v in args.__dict__.items() - if f.startswith('-') and v] - - # Configure Construct - logging_level = ('WARNING', 'DEBUG')[args.__dict__['-v']] - construct.init(host='cli', logging=logging_config(logging_level)) - construct.set_context_from_path(os.getcwd()) - - # Add all subcommands including contextual actions - subcommands = OrderedDict() - - for command in commands: - subcommands[command.name] = command(parser) - - for action in construct.actions: - subcommands[action.identifier] = ActionCommand(action, parser) - - # Print root help if we received no command argument - if not args.command: - parser.print_help() - sys.exit() - - command_name = args.command - command_args = root_flags + extra_args - - if command_name not in subcommands: - print('Command does not exist: ', command_name) - sys.exit(1) - - command = subcommands[command_name] - args, extra_args = command.parse(command_args) - args.__dict__.pop('verbose') - command.run(args, *extra_args) - - -if __name__ == '__main__': - main() diff --git a/construct/cli/commands.py b/construct/cli/commands.py deleted file mode 100644 index b561fb8..0000000 --- a/construct/cli/commands.py +++ /dev/null @@ -1,533 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function -import argparse -import os -import sys -import logging -import traceback -from textwrap import dedent -from scrim import get_scrim -import construct -from construct.errors import ActionControlFlowError, ArgumentError -from construct.cli.formatters import new_formatter, Contextual, format_section -from construct.cli.constants import OPTIONS_TITLE, ARGUMENTS_TITLE -from construct.cli.utils import styled, error, abort -from construct.utils import classproperty - - -_log = logging.getLogger(__name__) - - -class Command(object): - '''Base class for all CLI Commands. Override :meth:`setup_parser` to add - arguments to a command. Implement :meth:`run` to customize what happens - when the command is invoked. - ''' - - name = None - - def __init__(self, parent): - self.parent = parent - self.parser = argparse.ArgumentParser( - self.name, - formatter_class=new_formatter(self, Contextual) - ) - self.parser._optionals.title = OPTIONS_TITLE - self.parser._positionals.title = ARGUMENTS_TITLE - self.parser.add_argument( - '-v', - '--verbose', - help='verbose output', - action='store_true', - ) - self._parser_setup = False - - @classmethod - def _available(cls): - return cls.name and cls.available() - - @classmethod - def available(cls): - '''Implement this to set when a command is available.''' - return True - - @property - def arguments(self): - return self.parser._get_positional_actions() - - @property - def options(self): - return self.parser._get_optional_actions() - - @property - def usage(self): - usage = styled('{bright}{} {}', self.parent.prog, self.name) - for argument in self.arguments: - usage += styled(' {fg.blue}<{}>', argument.dest) - if self.options: - usage += styled(' {fg.yellow}[options]') - usage += styled('{reset}') - return usage - - @classproperty - def short_description(cls): - return cls.__doc__.split('\n')[0] - - @classproperty - def description(cls): - parts = cls.__doc__.split('\n') - short = parts[0] - body = dedent('\n'.join(parts[1:])) - text = '\n'.join([short, body]) - return text - - def _setup_parser(self, parser): - if not self._parser_setup: - self.setup_parser(self.parser) - self._parser_setup = True - - def setup_parser(self, parser): - return NotImplemented - - def parse(self, args): - self._setup_parser(self.parser) - args, extra_args = self.parser.parse_known_args(args) - return args, extra_args - - def run(self, args, *extra_args): - return NotImplemented - - -class Version(Command): - '''Version information''' - - name = 'version' - - def run(self, args, *extra_args): - section = format_section( - '', - [ - ('', ''), - ('name', construct.__title__), - ('version', construct.__version__), - ('url', construct.__url__), - ('package', os.path.dirname(construct.__file__)), - ('config', os.environ.get('CONSTRUCT_CONFIG', 'default')), - ], - lcolor=styled('{bright}') - ) - print(section) - - -def requires_scrim(command): - '''Command becomes available only when cli run by scrim script... - - See also: - https://github.com/danbradham/scrim - ''' - - command.available = staticmethod(lambda: get_scrim().shell) - return command - - -@requires_scrim -class Home(Command): - '''Go to root directory''' - - name = 'home' - - def run(self, args, *extra_args): - ctx = construct.get_context() - - if not os.path.exists(ctx.root): - os.makedirs(ctx.root) - - scrim = get_scrim() - scrim.pushd(ctx.root) - - -@requires_scrim -class Pop(Command): - '''Go back to last location''' - - name = 'pop' - - def run(self, args, *extra_args): - scrim = get_scrim() - scrim.popd() - - -@requires_scrim -class Push(Command): - '''Go to first search result''' - - name = 'push' - - def setup_parser(self, parser): - parser.add_argument( - 'name', - nargs='?', - help='Name of Entry like: my_project or my_project/my_asset', - ) - parser.add_argument( - '-r', '--root', - default=os.getcwd(), - help='Root directory of search', - type=str, - ) - parser.add_argument( - '--up', - action='store_true', - default=False, - help='Search up tree instead of down', - dest='direction' - ) - parser.add_argument( - '-t', '--tags', - nargs='*', - help='List of tags like: project' - ) - parser.add_argument( - '-d', '--depth', - type=int, - required=False, - help='Search depth' - ) - - def run(self, args, *extra_args): - import fsfs - - ctx = construct.get_context() - - if not args.tags and args.name: - query = dict( - selector=args.name, - root=args.root, - skip_root=True - ) - entry = construct.quick_select(**query) - else: - query = dict( - root=args.root, - name=args.name, - tags=args.tags, - direction=args.direction, - depth=args.depth or (3 if ctx.project else 2), - skip_root=True - ) - # Get a better match, not just the first hit - entries = list(construct.search(**query)) - - if not entries: - error('Could not find entry...') - sys.exit(1) - - if len(entries) == 1: - entry = entries[0] - else: - # The shortest entry has to be the closest to our query - entry = min(entries, key=lambda e: len(e.path)) - - if not entry: - error('Could not find entry...') - sys.exit(1) - - path = entry.path - if args.name: - parts = args.name.split('/') - for part in parts: - highlight = styled('{bright}{fg.yellow}{}{reset}', part) - path = path.replace(part, highlight) - print(path) - - scrim = get_scrim() - scrim.pushd(os.path.abspath(entry.path)) - - -class Search(Command): - '''Search for Entries - - Examples: - construct search my_project - construct search my_project/my_asset - construct search -t asset - construct search -t workspace maya - ''' - - name = 'search' - - def setup_parser(self, parser): - parser.add_argument( - 'name', - nargs='?', - help='Name of Entry', - ) - parser.add_argument( - '-r', '--root', - default=os.getcwd(), - help='Root directory of search', - type=str, - ) - parser.add_argument( - '--up', - action='store_true', - default=False, - help='Search up tree instead of down', - dest='direction' - ) - parser.add_argument( - '-t', '--tags', - nargs='*', - help='List of tags like: project' - ) - parser.add_argument( - '-d', '--depth', - type=int, - required=False, - help='Search depth' - ) - - def run(self, args, *extra_args): - ctx = construct.get_context() - query = dict( - root=args.root, - name=args.name, - tags=args.tags, - direction=args.direction, - depth=args.depth or (3 if ctx.project else 1), - ) - entries = construct.search(**query) - - i = 0 - for i, entry in enumerate(entries): - path = entry.path - if args.name: - parts = args.name.split('/') - for part in parts: - highlight = styled('{bright}{fg.yellow}{}{reset}', part) - path = path.replace(part, highlight) - print(path) - - if i == 0: - print(('Found 0 result.')) - - -class Read(Command): - '''Read metadata''' - - name = 'read' - - def setup_parser(self, parser): - parser.add_argument( - '--root', '-r', - default=os.getcwd(), - help='Directory to read from' - ) - parser.add_argument('keys', nargs=-1, help='keys to read') - - def run(self, args, *extra_args): - import fsfs - data = fsfs.read(args.root, *args.keys) - print(fsfs.encode_data(data)) - - -class Write(Command): - '''Write metadata''' - - name = 'write' - - def setup_parser(self, parser): - parser.add_argument( - '--root', '-r', - default=os.getcwd(), - help='Directory to read from' - ) - parser.add_argument( - '-k', '--key', - dest='data', - required=True, - action='append', - nargs=2, - help='key value pairs to write' - ) - parser.add_argument( - '-d', '--delete', - dest='delkeys', - action='append', - help='Keys to delete', - nargs=1 - ) - - def run(self, args, *extra_args): - - import fsfs - from fsfs.cli import safe_eval - - entry = fsfs.get_entry(args.root) - if args.delkeys: - entry.remove(*args.delkeys) - - data = {k: safe_eval(v) for k, v in args.data} - - try: - entry.write(**data) - except Exception as e: - print('Failed to write data: ') - print(dict(data)) - print(e.message) - else: - print('Wrote data to ' + args.root) - - -class Tag(Command): - '''Tag a directory - - Examples: - construct tag project - construct tag task model - - Unless you know what you're doing, don't add tags. - ''' - - name = 'tag' - - def setup_parser(self, parser): - parser.add_argument( - '--root', '-r', - default=os.getcwd(), - help='Directory to tag' - ) - parser.add_argument('tags', nargs='*', help='List of tags to add') - - def run(self, args, *extra_args): - import fsfs - fsfs.tag(args.root, *args.tags) - - -class Untag(Command): - '''Untag a directory - - Examples: - construct untag project - construct untag task model - - Unless you know what you're doing, don't remove tags. - ''' - - name = 'untag' - - def setup_parser(self, parser): - parser.add_argument( - '--root', '-r', - default=os.getcwd(), - help='Directory to tag' - ) - parser.add_argument('tags', nargs='*', help='List of tags to remove') - - def run(self, args, *extra_args): - import fsfs - fsfs.untag(args.root, *args.tags) - - -class ActionCommand(Command): - ''':class:`Action` CLI command class''' - - def __init__(self, action, parent): - _log.debug('Initializing ActionCommand for %s' % action.identifier) - self.action = action - self.name = action.identifier - super(ActionCommand, self).__init__(parent) - - @property - def short_description(self): - return self.action.short_description - - @property - def description(self): - return self.action.description - - def custom_validator(self, name, cast, *validators): - def check_value(value): - value = cast(value) - if not all([v(value) for v in validators]): - msg = '%s: invalid value: %r' % (name, value) - raise abort(msg) - return value - return check_value - - def add_option(self, parser, param_name, param_options): - '''Add an argument to parser from an action parameter''' - param_flag = '--' + param_name - - # Handle bool options - if param_options['type'] is bool: - arg_spec = dict( - action='store_true', - dest=param_name, - help=param_options.get('help', None), - default=param_options.get('default', False), - ) - return parser.add_argument(param_flag, **arg_spec) - - # Handle all other options - arg_spec = dict( - action=('store', 'store_true')[param_options['type'] is bool], - type=param_options.get('type', str), - dest=param_name, - help=param_options.get('help', None), - # required=param_options.get('required', False), - default=param_options.get('default', None), - choices=param_options.get('options', None) - ) - - # Build a validator if we get a tuple of types, argparse calls - # type(arg), so our validator casts the argument first, then - # validates it. - cast = arg_spec['type'] - validators = param_options.get('validator', []) - if isinstance(param_options['type'], tuple): - cast = param_options['type'][0] - validators.append(lambda x: isinstance(x, param_options['type'])) - - if validators: - arg_spec['type'] = self.custom_validator( - param_flag, - cast, - *validators - ) - - parser.add_argument(param_flag, **arg_spec) - - def setup_parser(self, parser): - ctx = construct.get_context() - params = self.action.params(ctx) - for param_name, param_options in params.items(): - self.add_option(parser, param_name, param_options) - - def run(self, args, *extra_args): - try: - action = self.action(*extra_args, **args.__dict__) - action.run() - except ActionControlFlowError as e: - msg = styled( - '{bright}{fg.red}Control Flow Error:{fg.reset} {}' - ' raised critical error {fg.red}{}{reset}', - self.action.identifier, - e.__class__.__name__ - ) - print(msg) - except ArgumentError as e: - msg = styled('{bright}{fg.red}Argument Error:{fg.reset} {}', e) - print(msg) - except Exception as e: - msg = styled( - '{bright}{fg.red}Action Error:{fg.reset} {}' - ' raised unexpected error {fg.red}{}{reset}\n', - self.action.identifier, - e.__class__.__name__ - ) - print(msg) - traceback.print_exc() - - -commands = [c for c in Command.__subclasses__() if c._available()] diff --git a/construct/cli/constants.py b/construct/cli/constants.py deleted file mode 100644 index 7626b2b..0000000 --- a/construct/cli/constants.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from platform import platform -from construct.cli.utils import styled -from collections import OrderedDict -from construct.constants import ( - WAITING, - PENDING, - RUNNING, - SUCCESS, - FAILED, - SKIPPED, - PAUSED, - DISABLED, - ENABLED, -) - - -ACTION_CONTEXT_TITLE = styled('{normal}Action Context{reset}') -ACTIONS_TITLE = styled('{bright}{fg.blue}Actions{reset}') -ARGUMENTS_TITLE = styled('{bright}{fg.blue}Arguments{reset}') -ARTIFACTS_TITLE = styled('{bright}{fg.green}Artifacts{reset}') -COMMANDS_TITLE = styled('{bright}{fg.blue}Commands{reset}') -CONTEXT_TITLE = styled('{normal}Current Context{reset}') -OPTIONS_TITLE = styled('{bright}{fg.yellow}Options{reset}') - -STATUS_LABELS = { - WAITING: styled('{dim}WAITING{reset}'), - PENDING: styled('{dim}PENDING{reset}'), - RUNNING: styled('{fg.green}RUNNING{reset}'), - SUCCESS: styled('{bright}{fg.green}SUCCESS{reset}'), - FAILED: styled('{bright}{fg.red}FAILED{reset}'), - SKIPPED: styled('{fg.cyan}SKIPPED{reset}'), - PAUSED: styled('{fg.red}PAUSED{reset}'), - DISABLED: styled('{dim}DISABLED{reset}'), - ENABLED: styled('{bright}ENABLED{reset}'), -} -ICONS = OrderedDict([ - ('ENABLED', styled('{dim}{fg.green}◌{reset}')), - ('DISABLED', styled('{dim}{fg.red}◌{reset}')), - ('WAITING', styled('{dim}◌{reset}')), - ('RUNNING', styled('{fg.green}◌{reset}')), - ('PENDING', styled('{bright}{fg.magenta}◌{reset}')), - ('PAUSED', styled('{bright}{fg.cyan}◌{reset}')), - ('SKIPPED', styled('{bright}{fg.blue}◌{reset}')), - ('SUCCESS', styled('{bright}{fg.green}●{reset}')), - ('FAILED', styled('{bright}{fg.red}●{reset}')), -]) - - -if platform().startswith('Windows-7'): - # Use cp437 compatible characters on Windows 7 - ICONS = OrderedDict([ - ('ENABLED', styled('{dim}{fg.green}○{reset}')), - ('DISABLED', styled('{bright}{fg.red}○{reset}')), - ('WAITING', styled('{dim}○{reset}')), - ('RUNNING', styled('{fg.green}○{reset}')), - ('PENDING', styled('{bright}{fg.magenta}○{reset}')), - ('PAUSED', styled('{bright}{fg.cyan}○{reset}')), - ('SKIPPED', styled('{bright}{fg.blue}○{reset}')), - ('SUCCESS', styled('{bright}{fg.green}•{reset}')), - ('FAILED', styled('{bright}{fg.red}•{reset}')), - ]) diff --git a/construct/cli/formatters.py b/construct/cli/formatters.py deleted file mode 100644 index 5f08ba5..0000000 --- a/construct/cli/formatters.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import argparse -import construct -from construct.cli.constants import ( - ACTIONS_TITLE, - COMMANDS_TITLE, - CONTEXT_TITLE -) -from construct.cli.utils import style, styled - - -reset = style.reset - - -def new_formatter(parent, formatter_type): - return type( - formatter_type.__name__, - (formatter_type,), - dict( - parent=parent - ) - ) - - -def format_context(): - ctx = construct.get_context() - ctx_data = [] - for k in ctx.keys: - v = ctx[k] - if not v: - continue - if k in ctx.entry_keys: - ctx_data.append((k, v.name)) - else: - ctx_data.append((k, v)) - return format_section( - CONTEXT_TITLE, - ctx_data, - lcolor=styled('{bright}') - ) - - -def format_commands(): - from construct.cli.commands import commands - return format_section( - COMMANDS_TITLE, - [(c.name, c.short_description) for c in commands], - lcolor=styled('{bright}') - ) - - -def format_actions(): - actions = construct.actions.collect() - return format_section( - ACTIONS_TITLE, - [(a.identifier, a.description) for a in construct.actions], - lcolor=styled('{bright}') - ) - - -def format_section(title, data, indent='', lcolor=reset, rcolor=reset): - '''Format a section''' - - if isinstance(data, dict): - data = list(data.items()) - - width = max(max([len(k) for k, v in data]), 12) - line = indent + ' {lc}{:<{w}}{reset} {rc}{}{reset}' - - lines = [] - if title: - lines.append(indent + title) - for key, value in data: - lines.append(styled(line, key, value, w=width, lc=lcolor, rc=rcolor)) - return '\n'.join(lines) - - -class Contextual( - argparse.RawDescriptionHelpFormatter, - argparse.ArgumentDefaultsHelpFormatter -): - - def _format_action(self, action): - help = super(Contextual, self)._format_action(action) - sep = ' ' * self._indent_increment - parts = help.split(sep) - left = styled('{bright}{}{reset}', sep.join(parts[:-1])) - right = parts[-1] - return sep.join([left, right]) - - def add_usage(self, usage, actions, groups, prefix=None): - - parent = getattr(self, 'parent', None) - if parent: - usage = parent.usage - description = parent.description - else: - description = None - - prefix = 'Usage: ' - super(Contextual, self).add_usage(usage, actions, groups, prefix) - if description: - self.add_text('\n' + description) - # self.add_text(format_context() + '\n') - - def add_section(self, header, lines): - self.start_section(header) - self.add_lines(lines) - self.end_section() - - def add_lines(self, lines): - def format_lines(*args): - cind = self._current_indent * ' ' - text = cind + (('\n' + cind).join(args)) - return text - self._add_item(format_lines, lines) - - -class Root(Contextual): - - def add_usage(self, usage, actions, groups, prefix=None): - super(Root, self).add_usage(usage, actions, groups, prefix) - self.add_text(format_context() + '\n') - self.add_text(format_commands()) - self.add_text(format_actions()) diff --git a/construct/cli/stout.py b/construct/cli/stout.py deleted file mode 100644 index c412902..0000000 --- a/construct/cli/stout.py +++ /dev/null @@ -1,468 +0,0 @@ -# -*- coding: utf-8 -*- -''' -Terminal UI Framework -===================== -''' - -from __future__ import absolute_import, print_function -import re -import sys -import colorama -from backports.shutil_get_terminal_size import get_terminal_size -import win_unicode_console -import os - -_win_hide = None -_win_show = None -if os.name == 'nt': - # Taken from stackoverflow - # https://stackoverflow.com/a/10455937 - import msvcrt - import ctypes - - class _CursorInfo(ctypes.Structure): - _fields_ = [("size", ctypes.c_int), - ("visible", ctypes.c_byte)] - - def _win_hide(): - ci = _CursorInfo() - handle = ctypes.windll.kernel32.GetStdHandle(-11) - ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) - ci.visible = False - ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) - - def _win_show(): - ci = _CursorInfo() - handle = ctypes.windll.kernel32.GetStdHandle(-11) - ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) - ci.visible = True - ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) - - -TAB_WIDTH = 4 -orig_stdout = None -orig_stderr = None -_console = None - - -def get_console(): - return _console - - -def set_console(console): - global _console - _console = console - - -def clean_text(text): - from functools import reduce - substitutions = [('\n', ''), ('\r', ''), ('\t', ' ' * TAB_WIDTH)] - return reduce(lambda s, sre: str.replace(s, *sre), substitutions, text) - - -class AnsiToken(str): - - def __new__(cls, value, pattern): - inst = str.__new__(cls, value) - inst.pattern = re.compile(pattern) - return inst - - def __call__(self, *args): - return self.format(*args) - - def search(self, text): - return self.pattern.search(text) - - -class Ansi(object): - '''Ansi cursor movement parsing''' - - pattern = re.compile('\\x1b\[([\d+];[\d+]H|[\d+]?[ABCDK]|\?25[lh])') - - # Ansi cursor movement tokens - move = AnsiToken('\x1b[{};{}H', '\\x1b\[(\d+)\;(\d+)H') - move_up = AnsiToken('\x1b[{}A', '\\x1b\[(\d+)?A') - move_down = AnsiToken('\x1b[{}B', '\\x1b\[(\d+)?B') - move_right = AnsiToken('\x1b[{}C', '\\x1b\[(\d+)?C') - move_left = AnsiToken('\x1b[{}D', '\\x1b\[(\d+)?D') - clear = AnsiToken('\x1b[K', '\\x1b\[K') - hide_cursor = AnsiToken('\x1b[?25l', '\\x1b\[?25l') - show_cursor = AnsiToken('\x1b[?25h', '\\x1b\[?25h') - home = AnsiToken('\r', '\\r') - tokens = [t for t in locals().items() if isinstance(t[1], AnsiToken)] - - @classmethod - def get_token_args(cls, text): - '''Parses text returning method name and arguments. - - Examples: - >>> Ansi.get_token_args('\x1b[10;2H') - ('move', [10, 2]) - >>> Ansi.get_token_args('\x1b[4A') - ('move_up', [4]) - ''' - - for name, token in cls.tokens: - if token == '\r': - continue - match = token.search(text) - if match: - args = [int(g) for g in match.groups()] - return name, args - - @classmethod - def match(cls, text): - '''Returns True if the text matches ANY token in Ansi.tokens''' - - return cls.pattern.match(text) - - -class Cursor(object): - '''Provides terminal console cursor interaction''' - - def __init__(self, console): - self.console = console - self.dx = 0 - self.dy = 0 - - def show_cursor(self): - if _win_show: - _win_show() - else: - self.console.stream.write(Ansi.show_cursor) - - def hide_cursor(self): - if _win_hide: - _win_hide() - else: - self.console.stream.write(Ansi.hide_cursor) - - def home(self): - self.console.stream.write(Ansi.home) - self.dx = 0 - - def clear(self): - self.console.stream.write(Ansi.clear) - - def reset(self): - self.move(-self.dx, -self.dy) - - def move_up(self, y=1): - self.console.stream.write(Ansi.move_up(y)) - self.dy += y - - def move_down(self, y=1): - self.console.stream.write(Ansi.move_down(y)) - self.dy -= y - - def move_right(self, x=1): - self.console.stream.write(Ansi.move_right(x)) - self.dx += min(self.console.width, self.dx + x) - - def move_left(self, x=1): - self.console.stream.write(Ansi.move_left(x)) - self.dx = max(0, self.dx - x) - - def move(self, x, y): - if x > 0: - self.move_right(x) - else: - self.move_left(abs(x)) - if y > 0: - self.move_up(y) - else: - self.move_down(abs(y)) - - -class ConsoleStream(object): - '''Parse and redirect stream.write to console.write. - - This stream wrapper parses text written to a stream and splits it line by - line and sends it to Console. This allows Console to maintain a buffered - list of Line widgets. - ''' - - def __init__(self, stream, console): - self.stream = stream - self.console = console - - def __getattr__(self, attr): - return self.stream.__getattr__(attr) - - def write(self, value): - buffer = list(value) - indent = 0 - string = '' - while buffer: - string += buffer.pop(0) - indent = len(string) - len(string.lstrip()) - if len(string) == self.console.width - 2: - self.console.write(string + '-\n') - string = ' ' * indent - elif string[-1] in ['\r', '\n']: - self.console.write(string) - string = '' - elif Ansi.match(string): - name, args = Ansi.get_token_args(string) - cursor_meth = getattr(self.console.cursor, name) - cursor_meth(*args) - string = '' - - if string: - if Ansi.match(string): - name, args = Ansi.get_token_args(string) - cursor_meth = getattr(self.console.cursor, name) - cursor_meth(*args) - else: - self.console.write(''.join(string)) - - -class Console(object): - '''Wraps stdout and stderr and wraps every line you write as a - Line widget. Making it easy to modify previously printed lines. You - can also subclass Line and manually add your own widgets to the output - stream. - ''' - - def __init__(self): - - self.lines = [] - self.wrapper = None - self.stream = None - self.cursor = None - - # Terminal size used to split lines longer than console width - t = get_terminal_size() - self.width = t.columns - self.height = t.lines # TODO: cap self.lines at height - 2? - - # State from previous write used - # to determine whether print modifies - # a line or creates a new one - self._n = True # \n - self._r = False # \r - self._e = False # eol - - def __enter__(self): - self.init() - return self - - def __exit__(self, type, value, traceback): - self.deinit() - - def init(self): - if self.stream: - raise RuntimeError('Console already initialized.') - - global orig_stdout - global orig_stderr - win_unicode_console.enable() - colorama.init() - orig_stderr = sys.stderr - orig_stdout = sys.stdout - self.stream = sys.stdout - self.wrapper = ConsoleStream(sys.stdout, self) - sys.stdout = self.wrapper - sys.stderr = self.wrapper - self.cursor = Cursor(self) - set_console(self) - self.cursor.hide_cursor() - - def deinit(self): - if self.stream is None: - raise RuntimeError('Console has not been initialized.') - - self.cursor.show_cursor() - global orig_stdout - global orig_stderr - sys.stdout = orig_stdout - sys.stderr = orig_stderr - orig_stdout = None - orig_stderr = None - self.stream = None - self.wrapper = None - self.cursor = None - colorama.deinit() - win_unicode_console.disable() - - def set_state(self, n, r, e): - self._n, self._r, self._e = n, r, e - - def add_widget(self, widget): - '''Manually add a widget''' - - self.lines.insert(0, widget) - self.set_state(True, False, False) - self.stream.write(widget.text.rstrip('\n') + '\n') - - def insert_widget(self, index, widget): - '''Manually insert a widget - - Examples: - import stout, time - with stout.Console() as con: - print(100) - w = stout.Spinner(con) - con.insert_widget(1, w) # insert spinner above '100' - for i in range(20): - w.tick() - time.sleep(0.05) - ''' - - # Insert a blank line - sys.__stdout__.write('\n') - - # Add widget to fill blank line - self.lines.insert(index, widget) - - # Update all widgets - for line in self.lines: - line.update() - - def insert(self, index, text): - '''Inserts text at the given index. Index starts at the bottom of your - console, like this: - - 3. a line - 2. another line - 1. yet another line - 0. last line - ''' - - # Store lines up to index and after index - before_lines = self.lines[:index] - after_lines = self.lines[index:] - - # Create new lines - num_lines = len(self.lines) - print(text) - new_lines = self.lines[:len(self.lines) - num_lines] - - # Rebuild line ordering - self.lines = before_lines + new_lines + after_lines - - # Update all lines - for line in self.lines: - line.update() - - def write(self, value): - '''Always returns a Line, either a new Line widget or the line that - was modified. - ''' - - length = len(value) - dy = self.cursor.dy - dx = self.cursor.dx - - if dy > 0: # Cursor is in another line - - line = self.lines[dy - 1] - if dx: - line._text = clean_text( - line._text[:dx] + - value + - line._text[dx + length:] - ) - else: - line._text = clean_text(value + line._text[length:]) - - elif self._n: # Cursor is on new line - - line = Line(value, self) - self.lines.insert(0, line) - - elif self._r: # Cursor is at the start of the last line - - line = self.lines[0] - line._text = clean_text(value + line._text[length:]) - - else: # Cursor is somewhere on the last line - - line = self.lines[0] - if dx: - line._text = clean_text( - line._text[:dx] + - value + - line._text[dx + length:] - ) - else: - line._text = clean_text(value + line._text[length:]) - - # Set state for next print - if value.endswith('\n'): - self.set_state(True, False, False) - self.cursor.dx = 0 - elif value.endswith('\r'): - self.set_state(False, True, False) - self.cursor.dx = 0 - else: - self.set_state(False, False, True) - self.cursor.dx += length - - # Finally...print to stream - self.stream.write(value) - return line - - -class Line(object): - '''Base widget class''' - - def __init__(self, text, console): - self._text = clean_text(text) - self.console = console - - def __repr__(self): - return '{}(row={}, text={!r})'.format( - self.__class__.__name__, - self.row, - self.text - ) - - @property - def text(self): - return self._text - - @text.setter - def text(self, value): - self._text = clean_text(value) - self.update() - - @property - def row(self): - return self.console.lines.index(self) - - def update(self): - row = self.row + 1 - if row > self.console.height - 2: # Skip updates outside view - return - - cursor = self.console.cursor - cursor.move_up(row) - cursor.home() - cursor.clear() - self.console.stream.write(self._text) - cursor.move_down(row) - cursor.home() - - -class Spinner(Line): - - chars = '|/-\\' - interval = 0.2 - - def __init__(self, console): - self.chars = self.default - self.index = 0 - super(Spinner, self).__init__(self.chars[0], console) - - def tick(self): - self.index = (self.index + 1) % len(self.chars) - self.text = self.chars[self.index] - - -class PingPong(Spinner): - - chars = [ - '. ', '.. ', '... ', ' ... ', ' ...', ' ..', - ' .', ' ..', ' ...', ' ... ', '... ', '.. ' - ] diff --git a/construct/cli/utils.py b/construct/cli/utils.py deleted file mode 100644 index 5139046..0000000 --- a/construct/cli/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import colorama -import sys - - -def error(msg): - print(styled('{bright}{fg.red}Error! {reset}{}', msg)) - - -def abort(msg=''): - print(styled('{bright}{fg.yellow}Abort! {reset}{}', msg)) - sys.exit(1) - - -def extract_styles(obj): - '''Extract styles from a colorama ansi code obj''' - attrs = {k.lower(): v for k, v in obj.__dict__.items() if k.isupper()} - return type(obj.__class__.__name__, (), attrs) - - -class style(object): - fg = extract_styles(colorama.Fore) - bg = extract_styles(colorama.Back) - reset = colorama.Style.RESET_ALL - dim = colorama.Style.DIM - normal = colorama.Style.NORMAL - bright = colorama.Style.BRIGHT - - -def styled(string, *args, **kwargs): - '''Like string.format but includes ansi color styling keywords including - fg, bg, and style. These are aliases to colorama.Fore, colorama.Back and - colorama.Style. All codes should be referenced in lowercase. Finally - colorama.Style.RESET_ALL is aliased as reset. - - Examples: - >>> styled('{fg.red}WARNING:{reset}') # doctest: +SKIP - [WARNING] - ''' - - kwargs.update(vars(style)) - return string.format(*args, **kwargs) diff --git a/construct/cli/widgets.py b/construct/cli/widgets.py deleted file mode 100644 index 1a2bd71..0000000 --- a/construct/cli/widgets.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division -from construct.cli.stout import Line -from construct.cli.constants import ( - STATUS_LABELS, - ICONS -) -from construct.cli.utils import styled, style -from construct.constants import WAITING, FAILED -from construct import signals - - -class TaskLine(Line): - - template = ' {icon} {bright}{identifier}{sep}{reset}{description}' - error_template = ' {bright}{fg.red}{}: {reset}{}' - - def __init__(self, task, console): - self.task = task - self.status = WAITING - text = self.format_status(self.status) - super(TaskLine, self).__init__(text, console) - - def format_error(self, exc_info): - # Insert error message - exc_type, exc_value, exc_tb = exc_info - err = styled(self.error_template, exc_type.__name__, exc_value) - return err - - def format_status(self, status): - return styled( - self.template, - icon=ICONS[self.status], - identifier=self.task.identifier + style.reset, - description=self.task.description, - sep=' - ' - ) - - def set_status(self, status): - self.status = status - self.text = self.format_status(status) - - -class ProgressBar(Line): - - info_template = '{label} <{i} of {max}> ' - bar_template = '{bright}{color}{lfill}{dim}{rfill}{reset}' - lfill_char = '░' - rfill_char = '░' - color_map = [ - style.fg.red, - style.fg.yellow, - style.fg.green - ] - - def __init__(self, label, max, console): - self.label = label - self.max = max - self.width = min(76, console.width - 2) - self.i = 0 - text = self.format_bar() - super(ProgressBar, self).__init__(text, console) - - def get_color(self, percent): - from math import floor - index = int(floor(percent * (len(self.color_map) - 1))) - return self.color_map[index] - - def format_bar(self): - info = self.info_template.format( - label=self.label, - i=self.i, - max=self.max - ) - info_width = len(info) - bar_width = self.width - info_width - if self.i == 0: - percent = 0 - else: - percent = self.i / self.max - - lfill_width = int(bar_width * percent) - rfill_width = bar_width - lfill_width - return styled( - info + self.bar_template, - color=self.get_color(percent), - lfill=self.lfill_char * lfill_width, - rfill=self.rfill_char * rfill_width - ) - - def set_value(self, i): - self.i = i - self.text = self.format_bar() diff --git a/construct/compat.py b/construct/compat.py index 93e7703..8e0f56a 100644 --- a/construct/compat.py +++ b/construct/compat.py @@ -1,28 +1,31 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import +# Third party imports +import six -try: - basestring = basestring -except NameError: - basestring = (str, bytes) +try: + from pathlib2 import Path +except ImportError: + from pathlib import Path -def pkg_resources(): - '''Make sure we get the pkg_resources that installed construct. +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping - Nuke ships with setuptools-0.6c11-py26.egg - an extremely archaic version - of setuptools. In setuptools prior to the introduction of python wheels - pkg_resources found entry_points by parsing .egg-info directories. Nowadays - we install wheels and package metadata is stored in .dist-info directories. - ''' - from os.path import exists - from construct.utils import package_path, import_file +if six.PY2: + import functools + _assignments = functools.WRAPPER_ASSIGNMENTS + _updates = functools.WRAPPER_UPDATES + def wraps(wrapped, assigned=_assignments, updated=_updates): + members = dir(wrapped) + assigned = set(members) & set(assigned) + updated = set(members) & set(updated) + return functools.wraps(wrapped, assigned, updated) +else: + from functools import wraps - pkg_resources_path = package_path('../pkg_resources') - if not exists(pkg_resources_path): - # Fallback to standard import - import pkg_resources - return pkg_resources - pkg_resources = import_file(pkg_resources_path) - return pkg_resources +# Instead of python-future +basestring = six.string_types +reraise = six.reraise diff --git a/construct/config.py b/construct/config.py deleted file mode 100644 index be8e132..0000000 --- a/construct/config.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from construct.constants import DEFAULT_CONFIG -import yaml -try: - from collections import ChainMap -except ImportError: - from chainmap import ChainMap - - -class Config(object): - '''Config object that looks up values in the following order. - - 1. context.project.data - 2. config.dict - 3. config.defaults - ''' - - defaults = {} - - with open(DEFAULT_CONFIG, 'r') as f: - defaults.update(yaml.load(f.read())) - - def __init__(self, *args, **kwargs): - self.dict = dict(*args, **kwargs) - - def __str__(self): - return str(self.flatten()) - - def chain(self): - '''Build chainmap for current context''' - - dicts = [] - - from construct.api import get_context - ctx = get_context() - if ctx and ctx.project: - data = dict( - (k, v) for k, v in ctx.project.data.items() - if k.isupper() - ) - dicts.append(data) - - dicts.append(self.dict) - dicts.append(self.defaults) - return ChainMap(*dicts) - - def flatten(self): - return dict(self.chain()) - - def get(self, *args): - return self.chain().get(*args) - - def __getitem__(self, item): - return self.chain().__getitem__(item) - - def __setitem__(self, item, value): - self.dict.__setitem__(item, value) - - def update(self, *args, **kwargs): - self.dict.update(*args, **kwargs) diff --git a/construct/constants.py b/construct/constants.py index ba174b8..14a2ce3 100644 --- a/construct/constants.py +++ b/construct/constants.py @@ -1,40 +1,50 @@ # -*- coding: utf-8 -*- + from __future__ import absolute_import + +# Standard library imports import os -import timeit -import time -from construct.utils import package_path, unipath +import sys +# Local imports +from .utils import unipath -WAITING = 'WAITING' -PENDING = 'PENDING' -RUNNING = 'RUNNING' -SUCCESS = 'SUCCESS' -FAILED = 'FAILED' -SKIPPED = 'SKIPPED' -PAUSED = 'PAUSED' -DISABLED = 'DISABLED' -ENABLED = 'ENABLED' -DONE_STATUSES = [SUCCESS, FAILED] -ACTION_SIGNALS = [ - 'action.*', - 'group.*', - 'request.*', -] +PLATFORM = sys.platform.rstrip('1234567890') +if PLATFORM == 'darwin': + PLATFORM = 'mac' EXTENSIONS_ENTRY_POINT = 'construct.extensions' -USER_CONFIG = unipath('~/.construct/construct.yaml') -DEFAULT_CONFIG = package_path('defaults', 'construct.yaml') -DEFAULT_ROOT = unipath('~/projects') -DEFAULT_HOST = 'standalone' +SETTINGS_FILE = 'construct.yaml' +USER_SETTINGS_FILE = unipath('~/.construct/construct.yaml') +USER_PATH = unipath('~/.construct') + +DEFAULT_API_NAME = 'global' +DEFAULT_PATHS = [ + unipath('~/.construct'), +] +DEFAULT_LOCATION = 'local' +DEFAULT_MOUNT = 'projects' +DEFAULT_LOCATIONS = { + 'local': { + 'projects': unipath('~/projects').as_posix(), + 'lib': unipath('~/lib').as_posix() + } +} +DEFAULT_TREE = { + 'asset': '{mount}/{project}/{bin}/{asset_type}/{asset}', + 'workspace': '{mount}/{project}/{bin}/{asset}/work/{task}/{host}', + 'publish': '{mount}/{project}/{bin}/{asset}/publish/{item}/v{version:0>3d}', + 'review': '{mount}/{project}/{bin}/{asset}/review/{task}/{host}', + 'render': '{mount}/{project}/{bin}/{asset}/render/{task}/{host}', + 'file': '{task_short}_{name}_v{version:0>3d}.{ext}', + 'file_sequence': '{task_short}_{name}_v{version:0>3d}.{frame}.{ext}', +} DEFAULT_LOGGING = dict( version=1, - formatters={ - 'simple': { - 'format': '%(levelname).1s:%(name)s> %(message)s' - } - }, + formatters={'simple': { + 'format': '%(levelname).1s:%(name)s> %(message)s' + }}, handlers={ 'console': { 'class': 'logging.StreamHandler', @@ -45,12 +55,13 @@ 'construct': { 'level': os.environ.get('CONSTRUCT_LOGGING_LEVEL', 'DEBUG'), 'handlers': ['console'], + 'propagate': False, } - } -) - -FSFS_DATA_ROOT = '.data' -FSFS_DATA_FILE = 'data' - -TIMER = timeit.default_timer -SLEEP = time.sleep + }) +DEFAULT_SETTINGS = { + 'my_location': DEFAULT_LOCATION, + 'my_mount': DEFAULT_MOUNT, + 'locations': DEFAULT_LOCATIONS, + 'logging': DEFAULT_LOGGING, +} +DEFAULT_HOST = 'standalone' diff --git a/construct/context.py b/construct/context.py index c3adc1f..3c787b0 100644 --- a/construct/context.py +++ b/construct/context.py @@ -1,174 +1,162 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -__all__ = [ - 'Context', - '_ctx_stack', - '_req_stack', -] +from __future__ import absolute_import +# Standard library imports +import copy +import getpass +import json import os -import fsfs -from getpass import getuser -from collections import Mapping -from werkzeug.local import LocalStack -from construct.constants import DEFAULT_ROOT, DEFAULT_HOST -from construct.utils import platform, unipath -from construct.models import is_entry +# Local imports +from .compat import basestring +from .constants import DEFAULT_HOST, PLATFORM -_ctx_stack = LocalStack() -_req_stack = LocalStack() +__all__ = ['Context'] -class Context(object): - keys = [ - 'host', - 'root', - 'user', - 'project', - 'collection', - 'sequence', - 'shot', - 'asset_type', - 'asset', - 'task', - 'workspace', - 'platform', - 'file' - ] - entry_keys = [ - 'project', - 'collection', - 'sequence', - 'shot', - 'asset_type', - 'asset', - 'task', - 'workspace', - 'file' - ] - defaults = {k: None for k in keys} - defaults['platform'] = platform - defaults['user'] = getuser() +def encode(obj): + '''Encode objects to stuff in environment variables''' - def __init__(self, **kwargs): - kwargs = dict(self.defaults, **kwargs) - self.__dict__.update(kwargs) + return json.dumps(obj) - def __repr__(self): - kwargs = ', '.join( - [('{}={}').format(k, v) for k, v in self.__dict__.items()] - ) - return '{}({})'.format(self.__class__.__name__, kwargs) - def __enter__(self): - self.push() - return self +def decode(obj): + '''Decode objects stored in environment variables''' - def __exit__(self, exc_type, exc_value, tb): - self.pop() + return json.loads(obj) - def __getitem__(self, key): - return self.__dict__.__getitem__(key) - def __setitem__(self, key, value): - self.__dict__.__setitem__(key, value) +class Context(dict): + '''Represents a state used to interact with Construct. The Construct object + provides both item and attribute access. - def __contains__(self, key): - return self.__dict__.__contains__(key) + >>> ctx = Context() + >>> ctx.project = {'name': 'my_project'} + >>> ctx['project'] == ctx.project - def push(self): - _ctx_stack.push(self) + Context objects can be loaded from and stored in environment variables + prefixed with **CONSTRUCT_**. - def pop(self): - if _ctx_stack.top is self: - _ctx_stack.pop() + +-----------+------------------------------------------------------+ + | key | description | + +===========+======================================================+ + | user | usually the same as the logged in user | + +-----------+------------------------------------------------------+ + | platform | Current platform [win, linux, mac] | + +-----------+------------------------------------------------------+ + | host | Host application like maya, nuke, standalone, etc... | + +-----------+------------------------------------------------------+ + | location | Location name | + +-----------+------------------------------------------------------+ + | mount | Mount name | + +-----------+------------------------------------------------------+ + | project | Project name | + +-----------+------------------------------------------------------+ + | bin | Bin name | + +-----------+------------------------------------------------------+ + | asset | Asset name | + +-----------+------------------------------------------------------+ + | task | Task name | + +-----------+------------------------------------------------------+ + | workspace | Directory of current workspace | + +-----------+------------------------------------------------------+ + | file | Path to current working file | + +-----------+------------------------------------------------------+ - def get_deepest_entry(self): - for key in reversed(self.entry_keys): - entry = self.__dict__.get(key, None) - if entry and is_entry(entry): - return entry + The above keys default to None so when checking context it can be + convenient to use attribute access. - def update(self, other, exclude=['host']): - if isinstance(other, Context): - other = other.__dict__ + >>> if ctx.project and ctx.task: + ... # Do something that depends on a project and task - if isinstance(other, Mapping): - if exclude: - other = {k: v for k, v in other.items() if k not in exclude} - self.__dict__.update(other) - else: - raise TypeError('Not a Mapping or Context: ', other) + ''' - def to_env_dict(self, exclude=None): - '''Push context to environment variables...''' + _keys = [ + 'platform', + 'user', + 'host', + 'location', + 'mount', + 'project', + 'bin', + 'asset', + 'task', + 'workspace', + 'file', + ] + _defaults = {k: None for k in _keys} + _defaults['user'] = getpass.getuser() + _defaults['platform'] = PLATFORM + _defaults['host'] = DEFAULT_HOST + + def __init__(self, **kwargs): + self.update(**self._defaults) + self.update(**kwargs) - exclude = exclude or ['host'] - data = {} - for key in self.keys: + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) - if key in exclude: - continue + def __setattr__(self, name, value): + self[name] = value - value = getattr(self, key) - if value is not None: - data['CONSTRUCT_' + key.upper()] = str(value) - return data + def copy(self): + '''Copy this context''' - def to_env(self, exclude=None): - '''Push context to environment variables...''' + return self.__class__(**copy.deepcopy(self)) - exclude = exclude or ['host'] + def load(self, env=None, **kwargs): + '''Update ctx with values stored in environment variables. You can + optionally pass a dictionary to load from. This is different than using + update because it only updates standard context keys rather than + updating all keys. + ''' - for key in self.keys: + env = env or os.environ + self.update(**kwargs) - if key in exclude: - continue + for key in self._keys: + env_key = ('construct_' + key).upper() + value = env.get(env_key, None) + if value: + try: + self[key] = decode(value) + except ValueError: + self[key] = value - value = getattr(self, key) - if value is not None: - os.environ['CONSTRUCT_' + key.upper()] = str(value) + def unload(self): + '''Restore this Context to defaults''' - @classmethod - def from_env(cls, exclude=None): - '''Create new context from environment variables''' + self.clear() + self.update(**self._defaults) - exclude = exclude or [] - data = dict( - root=os.environ.get('CONSTRUCT_ROOT', DEFAULT_ROOT), - host=os.environ.get('CONSTRUCT_HOST', DEFAULT_HOST), - ) + def store(self, env=None): + '''Store this Context in os.environ. You may also pass a dict.''' - for entry in cls.entry_keys: + env = env or os.environ + env.update(self._to_env()) - if entry in exclude: - continue + def _to_env(self): + '''Serialize ctx to strings storable as environment values.''' - env_var = 'CONSTRUCT_' + entry.upper() - value = os.environ.get(env_var, None) + env = {} + for key in self._keys: + env_key = ('construct_' + key).upper() + value = self.get(key, None) if value: - value = fsfs.get_entry(value) - data[entry] = value - - return cls(**data) + if isinstance(value, basestring): + env[env_key] = value + else: + env[env_key] = encode(value) + return env @classmethod - def from_path(cls, path): - '''Extract context from file path''' - - ctx = Context.from_env(exclude=cls.entry_keys) - - if os.path.isfile(path): - ctx.file = unipath(path) - path = os.path.dirname(path) - - for entry in fsfs.search(path, direction=fsfs.UP): - tags = entry.tags - for key in Context.entry_keys: - if key in tags: - setattr(ctx, key, entry) - - return ctx + def clear_env(cls, env=None): + env = env or os.environ + for key in cls._keys: + env_key = ('construct_' + key).upper() + os.environ.pop(env_key, None) diff --git a/construct/defaults/construct.yaml b/construct/defaults/construct.yaml deleted file mode 100644 index 0f56601..0000000 --- a/construct/defaults/construct.yaml +++ /dev/null @@ -1,195 +0,0 @@ - -ROOT: '~/projects' -EXTENSION_PATHS: - - '~/.construct' -EVENT_STREAM_DB: '~/.construct/eventstream.sqlite' -DISABLE_EXTENSIONS: [] - -LOGGING: - version: 1 - formatters: - simple: - format: '%(levelname).1s:%(name)s> %(message)s' - handlers: - console: - formatter: simple - class: logging.StreamHandler - loggers: - construct: - level: DEBUG - handlers: [console] - propagate: False - - -STATUSES: - waiting: - color: [0.0, 0.0, 0.0] - icon: 'default' - order: 0 - in progress: - color: [0.5, 0.5, 0.0] - icon: 'default' - order: 1 - rendering: - color: [0.0, 1.0, 0.0] - icon: 'default' - order: 2 - pending: - color: [0.5, 0.0, 0.5] - icon: 'default' - order: 3 - rejected: - color: [1.0, 0.0, 0.0] - icon: 'default' - order: 4 - complete: - color: [0.0, 0.0, 1.0] - icon: 'default' - order: 5 - - -TASK_TYPES: - model: - short: mdl - color: [0, 0, 0] - icon: 'default' - rig: - short: rig - color: [0, 0, 0] - icon: 'default' - shade: - short: shd - color: [0, 0, 0] - icon: 'default' - texture: - short: txt - color: [0, 0, 0] - icon: 'default' - comp: - short: comp - color: [0, 0, 0] - icon: 'default' - light: - short: lgt - color: [0, 0, 0] - icon: 'default' - layout: - short: lo - color: [0, 0, 0] - icon: 'default' - anim: - short: anim - color: [0, 0, 0] - icon: 'default' - fx: - short: fx - color: [0, 0, 0] - icon: 'default' - - -PATH_TEMPLATES: - collection: '{project}/{collection}' - asset_type: '{project}/{collection}/{asset_type}' - asset: '{project}/{collection}/{asset_type}/{asset}' - sequence: '{project}/{collection}/{sequence}' - shot: '{project}/{collection}/{sequence}/{shot}' - task: '{parent}/{task}' - publish: '{task}/publish/{file_type}/{task:[^_/]+}_{name}/v{versio:\d+}' - publish_file: '{task:[^_/]+}_{name}_v{version:\d+}{ext:\.\w+}' - workspace: '{task}/work/{workspace}' - workspace_file: '{task:[^_/]+}_{name}_v{version:\d+}{ext:\.\w+}' - - -SOFTWARE: - maya2018: - host: 'maya' - identifier: 'launch.maya' - description: 'Launch Autodesk Maya2018' - default_workspace: 'maya' - icon: './icons/maya.png' - extensions: ['.mb', '.ma'] - cmd: - win: - path: C:/Program Files/Autodesk/Maya2018/bin/maya.exe - args: [] - linux: - path: /usr/autodesk/maya2018/bin/maya - args: [] - mac: - path: /Applications/Autodesk/maya2018/bin/maya - args: [] - nuke11.1v3: - host: 'nuke' - identifier: 'launch.nuke' - description: 'Launch The Foundry Nuke11.1v3' - default_workspace: 'nuke' - icon: './icons/nuke.png' - extensions: ['.nk'] - cmd: - win: - path: C:/Program Files/Nuke11.1v3/Nuke11.1.exe - args: [] - linux: - path: /usr/local/Nuke11.1v3/Nuke11.1 - args: [] - mac: - path: /Applications/Nuke11.1v3/Nuke11.1v3.app/Nuke11.1 - args: [] - houdini16.5: - host: 'houdini' - identifier: 'launch.houdini' - description: 'Launch SideFX Houdini 16.5' - default_workspace: 'houdini' - icon: './icons/houdini.png' - extensions: ['.hip', '.hipnc'] - cmd: - win: - path: C:/Program Files/Side Effects Software/Houdini 16.5.496/bin/houdinifx.exe - args: [] - linux: - path: houdini - args: [] - mac: - path: houdini - args: [] - sublime3: - host: 'sublime' - identifier: 'launch.sublime' - description: 'Launch Sublimt Text 3' - default_workspace: null - icon: './icons/sublime.png' - extensions: ['.txt', '.py', '.mel'] - cmd: - win: - path: C:/Program Files/Sublime Text 3//subl.exe - args: [] - linux: - path: /usr/bin/subl - args: [] - mac: - path: /Applications/Sublime Text.app/Contents/SharedSupport/bin/subl - args: [] - - -FILE_TYPES: - alembic: - extensions: ['.abc'] - icon: 'default' - maya: - extensions: [ '.mb', '.ma'] - icon: 'default' - nuke: - extensions: ['.nk'] - icon: 'default' - c4d: - extensions: ['.c4d'] - icon: 'default' - photoshop: - extensions: ['.psd', '.psb'] - icon: 'default' - illustrator: - extensions: ['.ai'] - icon: 'default' - sequence: - extensions: ['.png', '.tif', '.exr', '.dpx'] - icon: 'default' diff --git a/construct/errors.py b/construct/errors.py index cbc7ff2..ff6ed37 100644 --- a/construct/errors.py +++ b/construct/errors.py @@ -1,122 +1,17 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -''' -errors and exceptions -===================== -All errors and exceptions in Construct are subclasses of ConstructError. -Errors that modify Action execution are subclasses of ActionControlFlowError. -''' -class ConstructError(Exception): - '''Base class for all Construct exceptions''' +class InvalidSettings(Exception): + '''Raise when your settings are invalid.''' -class InvalidPluginPath(ConstructError): - '''Raised when a plugin path does not exist''' +class ValidationError(Exception): + '''Raise when data has invalid keys and values.''' + def __init__(self, msg, errors): + super(ValidationError, self).__init__(msg) + self.errors = errors -class ParameterError(ConstructError): - '''Raise when a :attr:`Action.parameters` is defined incorrectly''' - -class RegistrationError(ConstructError): - '''Raised when a plugin raises an exception during registration''' - - -class ContextError(ConstructError): - '''Raised when an error in Context is encountered''' - - -class ConfigurationError(ConstructError): - '''Raised when an Entry or Construct is misconfigured''' - - -class ActionUnavailable(ConstructError): - '''Raised when an Action is sent even though it is unavailable in the - current context''' - - -class ActionError(ConstructError): - '''Raised when an action fails''' - - -class TaskError(ConstructError): - '''Raised when a task is misconfigured''' - - -class TemplateError(ConstructError): - '''Raised when a folder template, or path template error occurs''' - - -class ArgumentError(ConstructError): - '''Raised when an :class:`Action` is instantiated with arguments that do - not match the parameters specified in :attr:`Action.parameters`''' - - -class ActionControlFlowError(ConstructError): - ''' - Raised by Tasks to control Action execution... - ''' - - def __init__(self, *args, **kwargs): - import construct - self.ctx = construct.get_context() - self.request = construct.get_request() - super(ActionControlFlowError, self).__init__(*args, **kwargs) - - -class ValidationError(ActionControlFlowError): - '''Raised by a Task when validation of a staged item fails.''' - - def __init__(self, *args, **kwargs): - self.selection = kwargs.pop('selection', []) - super(ValidationError, self).__init__(*args, **kwargs) - - -class TimeoutError(ActionControlFlowError): - '''Raised when a request times out''' - - -class Skip(ActionControlFlowError): - ''' - Raise by a Task to notify the Action runner that the task should be - skipped. - ''' - - -class Disable(ActionControlFlowError): - ''' - Raise by a Task to notify the Action runner that the task should be - disabled. - ''' - - -class Abort(ActionControlFlowError): - ''' - Raised by a Task to notify the ActionRunner that a critical error has - occured. The ActionRunner will stop executing tasks and raise the error. - ''' - - -class Fail(ActionControlFlowError): - ''' - Raised by a Task to notify the ActionRunner that the Task has failed, but, - the ActionRunner should continue. - ''' - - -class Pause(ActionControlFlowError): - ''' - Raised by a Task to pause the ActionRunner. - ''' - - -class Confirm(ActionControlFlowError): - ''' - Raised by a Task to request confirmation of specific items from user. - ''' - - def __init__(self, items, *args, **kwargs): - self.items = items - super(Confirm, self).__init__(*args, **kwargs) +class ContextError(Exception): + '''Raise when a context is invalid or can not be extracted from a path.''' diff --git a/construct/events.py b/construct/events.py new file mode 100644 index 0000000..634b291 --- /dev/null +++ b/construct/events.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- + +# Standard library imports +import logging +from collections import OrderedDict, defaultdict +from operator import itemgetter + + +_log = logging.getLogger(__name__) + + +class EventManager(object): + '''The EventManager class maintains a priority list of handlers for events + to be executed in order when an event is sent. + + Examples: + >>> events = EventManager() + >>> events.define('my_event', 'My simple event') + >>> events.on('my_event', lambda msg: msg.upper()) + >>> events.send('my_event', 'Hello world!') + ['HELLO WORLD!'] + ''' + # TODO: Needs a better docstring + + def __init__(self): + self._events = OrderedDict() + self._priority_map = defaultdict(set) + self._handlers_map = defaultdict(dict) + + def load(self): + self.define('before_setup', '(api): Sent before api initialization.') + self.define('after_setup', '(api): Sent after api initialization.') + self.define('before_close', '(api): Sent before api uninitialization.') + self.define('after_close', '(api): Sent after api uninitialization.') + + def unload(self): + self.undefine('before_setup') + self.undefine('after_setup') + self.undefine('before_close') + self.undefine('after_close') + + def define(self, event, doc): + '''Define a new event. + + Arguments: + event (str): Name of the event + doc (str): Documentation for event + ''' + + _log.debug('Defining event %s', event) + self._events[event] = doc + + def undefine(self, event): + '''Undefine an event + + Arguments: + event (str): Name of the event + ''' + + _log.debug('Undefining event %s', event) + self._events.pop(event, None) + self.clear(event) + + def on(self, *args): + '''Adds a handler to the specified event. Can be used as a decorator. + + Examples: + >>> events = EventHandler() + >>> events.on('greet', lambda person: print('Hello %s' % person)) + + >>> @events.on('greet') + ... def greeter(person): + ... print('Hello %s' % person) + + Arguments: + event (str): Name of event + handler (callable): Function to add as handler + priority (int): Priority of handler + + Decorator Arguments: + event (str): Name of event + priority (int): Priority of handler + ''' + + argtypes = tuple([arg.__class__ for arg in args]) + + if len(args) >= 2 and callable(args[1]): + return self._on(*args) + elif argtypes in [(str,), (str, int)]: + return self._on_deco(*args) + else: + raise ValueError( + 'EventManager.on accepts the following signatures:\n' + ' (str, callable, [int]) when used normally\n' + ' OR (str, [int]) - when used as a decorator' + ) + + def _on(self, event, handler, priority=0): + if handler in self._handlers_map[event]: + return + + _log.debug('Added %s to event "%s"', handler, event) + entry = (priority, handler) + self._priority_map[event].add(entry) + self._handlers_map[event][handler] = entry + + def _on_deco(self, event, priority=0): + def _add_handler(handler): + self._on(event, handler, priority) + return handler + return _add_handler + + def off(self, event, handler): + '''Remove a handler from an event. + + Arguments: + event (str): Name of the event + handler (callable): Handler to remove + ''' + + if handler not in self._handlers_map[event]: + return + + _log.debug('Removed %s from event "%s"', handler, event) + entry = self._handlers_map[event].pop(handler) + self._priority_map[event].discard(entry) + + def send(self, event, *args, **kwargs): + '''Send an event. Executes all the event handlers and returns a + list of the handlers results. + + Arguments: + event (str): Name of event to send + *args: Event arguments + **kwargs: Event keyword arguments + ''' + + _log.debug('Sending event "%s"', event) + results = [] + for handler in self.handlers(event): + _log.debug('Calling %s', handler) + results.append(handler(*args, **kwargs)) + return results + + def handlers(self, event): + '''Get the handlers for an event in order of priority.''' + + sorted_handlers = sorted(self._priority_map[event], key=itemgetter(0)) + return [h[1] for h in sorted_handlers] + + def clear(self, event): + '''Remove all handlers from an event.''' + + self._handlers_map.pop(event, None) + self._priority_map.pop(event, None) + + def ls(self): + return list(self._events.items()) + + def show(self): + print('\n'.join([' '.join(e) for e in self.ls()])) diff --git a/construct/ext/__init__.py b/construct/ext/__init__.py new file mode 100644 index 0000000..704fe18 --- /dev/null +++ b/construct/ext/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Local imports +from .cache import Cache, UserCache +from .software import Software + + +extensions = [ + Software, + Cache, + UserCache, +] diff --git a/construct/ext/cache.py b/construct/ext/cache.py new file mode 100644 index 0000000..c689aed --- /dev/null +++ b/construct/ext/cache.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging +import shutil + +# Local imports +from ..constants import USER_PATH +from ..extensions import Extension +from ..utils import yaml_dump, yaml_load + + +_log = logging.getLogger(__name__) +missing = object() + + +class Cache(Extension): + '''A global cache shared between multiple users.''' + + identifier = 'cache' + label = 'Cache' + + def is_available(self, ctx): + return True + + def load(self, api): + api.extend(self.identifier, self) + # TODO: Switch cache backend based on settings + # this could be mongodb or redis eventually + self.cache = FSCache(api.settings.folder / 'cache') + + def unload(self, api): + api.unextend(self.identifier) + + def __contains__(self, key): + return self.cache.__contains__(key) + + def get(self, key, default=missing): + return self.cache.get(key, default) + + def set(self, key, value): + return self.cache.set(key, value) + + def delete(self, key): + return self.cache.delete(key) + + def clear(self): + return self.cache.clear() + + +class UserCache(Cache): + '''The local user cache. + + Applies to only one user. Can be used to store things like UI state, + favorites, and preferences. + ''' + + identifier = 'user_cache' + label = 'User Cache' + + def is_available(self, ctx): + return True + + def load(self, api): + api.extend(self.identifier, self) + self.cache = FSCache(USER_PATH / 'user') + + def unload(self, api): + api.unextend(self.identifier) + + +class FSCache(object): + '''Base implementation for filesystem Caches. + + Operates in a directory on the file system. Each key represents a single + file within the directory. + ''' + + def __init__(self, cache_dir): + self.cache_dir = cache_dir + self.cache_dir.mkdir(parents=True, exist_ok=True) + + def __contains__(self, key): + return self._file_for(key).exists() + + def _file_for(self, key): + return self.cache_dir / key + + def get(self, key, default=missing): + key_file = self._file_for(key) + if not key_file.exists(): + if default is not missing: + return default + else: + raise KeyError('Can not find %s in cache.' % key) + data = key_file.read_text(encoding='utf-8') + return yaml_load(data) + + def set(self, key, value): + key_file = self._file_for(key) + key_file.write_bytes(yaml_dump(value)) + + def delete(self, key): + key_file = self._file_for(key) + if key_file.exists(): + key_file.unlink() + + def clear(self): + if self.cache_dir.is_dir(): + shutil.rmtree(str(self.cache_dir)) diff --git a/construct/ext/software.py b/construct/ext/software.py new file mode 100644 index 0000000..59cb06b --- /dev/null +++ b/construct/ext/software.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging +import os +import subprocess +import sys + +# Local imports +from ..compat import basestring +from ..extensions import Extension +from ..utils import get_lib_path, update_dict, update_env + + +platform = sys.platform.rstrip('1234567890') +if platform == 'darwin': + platform = 'mac' + +_log = logging.getLogger(__name__) + + +class Software(Extension): + + identifier = 'software' + label = 'Software Extension' + + def is_available(self, ctx): + return ctx.project + + def load(self, api): + api.settings.add_section('software', 'software') + api.extend('software', self) + api.extend('launch', self.launch) + api.extend('open_with', self.open_with) + api.define( + 'before_launch', + '(api, software, env, ctx): Called before application launch.' + ) + api.define( + 'after_launch', + '(api, ctx): Called after an application is launched.' + ) + + def unload(self, api): + api.unextend('software') + api.unextend('launch') + api.unextend('open_with') + + @property + def folder(self): + return self.api.settings['software'].folder + + @property + def software_settings(self): + return self.api.settings['software'] + + def __contains__(self, key): + return key in self.software_settings + + def _get_project_software(self, project): + if 'software' not in project: + project = self.api.io.get_project_by_id(project['_id']) + return project.get('software', {}) + + def _available_software(self, software, ext=None): + available = {} + for software_name, software in software.items(): + if ext and ext not in software['extensions']: + continue + available[software_name] = software + return available + + def get_by_name(self, name, project=None, file=None): + try: + return self.get(project, file)[name] + except KeyError: + raise NameError('Could not find software named ' + name) + + def get(self, project=None, file=None): + '''Get the available software. + + If a project is provided get only software available in that project. + + If there is a project in the current context only return software in the + project's "software" dict. If a file is provided only return software + that can handle the file's extension. + + If a file is provided, only return software that can open that file. + ''' + + ext = None + project = project or self.api.context.project + + if file: + ext = os.path.splitext(file)[-1] + + if project: + if isinstance(project, basestring): + project = self.api.io.get_project(name=project) + software = self._get_project_software(project) + return self._available_software(software, ext) + + return self._available_software(self.software_settings, ext) + + def save(self, name, data, project=None): + '''Save software to settings or a project.''' + + if project: + software = self._get_project_software(project) + software[name] = self.software_settings.validate(data) + self.api.io.update_project(project, {'software': software}) + return software[name] + + return self.software_settings.write(name, data) + + def update(self, name, data, project=None): + '''Update software stored in settings or a project.''' + + if project: + # Get updated project software + software = self._get_project_software(project) + software_data = software.get(name, {}) + + # Update software data + update_dict(software_data, data) + + # Validate software data + software_data = self.software_settings.validate(software_data) + software[name] = software_data + + # Update project with software + self.api.io.update_project(project, {'software': software}) + return software[name] + + software_data = self.software_settings.get(name, {}) + update_dict(software_data, data) + self.software_settings.write(name, software_data) + return software_data + + def delete(self, name, project=None): + '''Delete software by name from settings or a project.''' + + if project: + software = self._get_project_software(project) + if name not in software: + return + software.pop(name) + self.api.io.update_project(project, {'software': software}) + + self.software_settings.delete(name) + + def launch(self, name, *args, **kwargs): + '''Launch the specified software. + + Arguments: + name (str): Name of software to launch + args: Arguments to pass to software executable + context: Context to launch software in + ''' + + software = self.get_by_name(name) + cmd = _get_command(software) + + ctx = kwargs.pop('context', self.api.context).copy() + args = list(args or software['args']) + cmd = [cmd] + args + env = _get_software_env(software, ctx, self.api.path) + + # Let Host Extension add to the environment + host = self.api.extensions.get(software['host'], None) + if host: + _log.debug('Executing before_launch: %s' % host) + host.before_launch(self.api, software, env, ctx) + else: + _log.debug( + 'Skipping before_launch...' + '%s has no registered ext.' % software['host'] + ) + + _log.debug('Launching %s' % name.title()) + _run(cmd, env=env) + + def open_with(self, name, file, *args, **kwargs): + '''Open a file with the specified software. + + Arguments: + name (str): Name of software to launch + file (str): File to open with software + *args: Arguments to pass to software executable + ''' + + software = self.get_by_name(name) + cmd = _get_command(software) + + ext = os.path.splitext(file)[-1] + if ext not in software['extensions']: + raise OSError('Can not open %s with %s' % (name, file)) + + ctx = kwargs.pop('context', self.api.context).copy() + ctx.file = file + args = list(args or software['args']) + cmd = [cmd] + args + [file] + env = _get_software_env(software, ctx, self.api.path) + + # Let Host Extension add to the environment + host = self.api.extensions.get(software['host'], None) + if host: + _log.debug('Executing before_launch: %s' % host) + host.before_launch(self.api, software, env, ctx) + else: + _log.debug( + 'Skipping before_launch...' + '%s has no registered ext.' % software['host'] + ) + + _log.debug('Launching %s' % name.title()) + _run(cmd, env=env) + + +def _get_command(software): + '''Get the platform specific command used to execute a piece of software.''' + + cmd = software['cmd'].get(platform, None) + if isinstance(cmd, basestring): + if os.path.isfile(cmd): + return cmd + for item in cmd: + if os.path.isfile(item): + return item + raise OSError( + "Could not find executable for %s. Are you sure it's installed?" + % software['name'] + ) + + +def _get_software_env(software, ctx, path): + '''Get the environment for the specified software.''' + + env = os.environ.copy() + update_env(env, **ctx.to_envvars()) + update_env(env, **software['env']) + update_env( + env, + CONSTRUCT_PATH=os.pathsep.join([str(p) for p in path]), + CONSTRUCT_HOST=software['host'], + PYTHONPATH=[get_lib_path().as_posix()] + ) + return env + + +def _run(*args, **kwargs): + '''On Windows start a detached process in it's own process group.''' + + if platform == 'win': + create_new_process_group = 0x00000200 + detached_process = 0x00000008 + creation_flags = detached_process | create_new_process_group + kwargs.setdefault('creationflags', creation_flags) + + kwargs.setdefault('shell', True) + subprocess.Popen(*args, **kwargs) diff --git a/construct/extension.py b/construct/extension.py deleted file mode 100644 index 08f0ef9..0000000 --- a/construct/extension.py +++ /dev/null @@ -1,388 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import abc -import os -import inspect -import logging -from collections import defaultdict -from fnmatch import fnmatch - -from construct.constants import EXTENSIONS_ENTRY_POINT -from construct.types import ABC -from construct.utils import iter_modules, ensure_type, missing, unipath -from construct.action import get_action_identifier, Action -from construct.compat import pkg_resources -pkg_resources = pkg_resources() - - -_extensions = {} -_log = logging.getLogger(__name__) - - -class Config(object): - - def __init__(self, key, default=missing): - self.key = key - self.default = default - - def __get__(self, obj, type=None): - from construct.api import config - - if obj is None: - return self - if self.default is missing: - return config[self.key] - else: - return config.get(self.key, self.default) - - -class Extension(ABC): - - name = None - attr_name = None - - @abc.abstractproperty - def name(self): - '''Name of this extension like: "Builtins"''' - - @abc.abstractproperty - def attr_name(self): - '''Nice name used for attribute access like: "builtins"''' - - def __init__(self): - self.enabled = True - self._actions = {} - self._tasks = defaultdict(list) - self._template_paths = [] - self._forms = {} - - def _available(self, ctx=missing): - if ctx is not missing: - return self.available(ctx) - return True - - def available(self, ctx): - return True - - def _load(self): - '''Load extension and call user implemented load''' - self.load() - - def _unload(self): - '''Unload extension and call user implemented unload''' - self.unload() - - def load(self): - '''Called by Construct when loading the extension. - - This method should register actions and tasks and perform any - necessary setup. - ''' - - def unload(self): - '''Called by Construct when unloading the extension. - - This method should unregister actions and tasks and perform any - necessary teardown. - ''' - - def add_template_path(self, path): - self._template_paths.append(unipath(path)) - - def remove_template_path(self, path): - p = unipath(path) - if p in self._template_paths: - self._template_paths.remove(p) - - def get_template_paths(self): - return list(self._template_paths) - - def add_form(self, action_or_identifier, form): - identifier = get_action_identifier(action_or_identifier) - self._forms[identifier] = form - - def remove_form(self, action_or_identifier, form): - identifier = get_action_identifier(action_or_identifier) - if self._forms[identifier] == form: - self._forms.pop(identifier) - - def get_form(self, action_or_identifier): - identifier = get_action_identifier(action_or_identifier) - return self._forms.get(identifier, None) - - def add_action(self, action): - '''Add an action to this extension''' - ensure_type(action, Action) - - if action.identifier not in self._actions: - self._actions[action.identifier] = action - - def remove_action(self, action): - '''Remove an action from this extensions''' - ensure_type(action, Action) - - if action.identifier in self._actions: - self._actions.pop(action.identifier) - - def get_actions(self, ctx=missing): - '''Get all actions for the specified ctx''' - - if ctx is missing: - return dict(self._actions) - - return { - identifier: action - for identifier, action in self._actions.items() - if action._available(ctx) - } - - def add_task(self, action_or_identifier, task, **task_overrides): - '''Add a task to the specified action''' - - identifier = get_action_identifier(action_or_identifier) - if task_overrides: - task = task.clone(**task_overrides) - self._tasks[identifier].append(task) - - def remove_task(self, action_or_identifier, task, **task_overrides): - '''Remove a task from the specified action''' - - identifier = get_action_identifier(action_or_identifier) - if task in self._tasks[identifier]: - self._tasks.remove(task) - - def get_tasks(self, identifier, ctx=missing): - '''Get all tasks for the spcified action ''' - - all_tasks = list(self._tasks[identifier]) - - for key, tasks in self._tasks.items(): - if key == identifier: - continue - if fnmatch(identifier, key): - all_tasks.extend(tasks) - - if ctx is missing: - return list(all_tasks) - - return [t for t in all_tasks if t.available(ctx)] - - -class HostExtension(Extension): - - name = 'Nice Name' - host_name = 'name' - - def modified(self): - '''Check if current file is modified''' - return NotImplemented - - def save_file(self, file): - '''Save a file''' - return NotImplemented - - def open_file(self, file): - '''Open a file''' - return NotImplemented - - def get_selection(self): - '''Get the host's current selection''' - return NotImplemented - - def set_selection(self, selection): - '''Set the host's current selection''' - return NotImplemented - - def get_workspace(self): - '''Get the host's workspace''' - return NotImplemented - - def set_workspace(self, directory): - '''Set the host's workspace''' - return NotImplemented - - def get_filepath(self): - '''Get the path to the host's current file''' - return NotImplemented - - def get_filename(self): - '''Get the name of the host's current file''' - return NotImplemented - - def get_frame_rate(self): - '''Get the frame rate of the host - - Returns: - float - fps - ''' - return NotImplemented - - def set_frame_rate(self, fps): - '''Set the frame rate of the host - - Arguments: - fps (float) - Frames per second - ''' - return NotImplemented - - def get_frame_range(self): - '''Get the frame range of the host - - Returns: - [min_frame, start_frame, end_frame, max_frame] - ''' - return NotImplemented - - def set_frame_range(self, min, start, end, max): - '''Set the frame range of the host - - Arguments: - min_frame (int): Min frame of timeline - start_frame (int): Start frame of animation - end_frame (int): End frame of animation - max_frame (int): Max frame of timeline - ''' - return NotImplemented - - def get_qt_parent(self): - '''Get the host's main QT widget''' - return NotImplemented - - def get_qt_app(self): - from Qt import QtWidgets - return QtWidgets.QApplication.instance() - - -class ExtensionCollector(object): - '''Handles Extension discovery and registration. Also allows looking - up extensions via attribute access.''' - - def __init__(self): - self.by_name = {} - self.by_attr = {} - - def __getattr__(self, name): - if name in self.by_name: - return self.by_name[name] - - if name in self.by_attr: - return self.by_attr[name] - - return self.__getattribute__(name) - - def __getitem__(self, name): - return getattr(self, name) - - def __iter__(self): - for ext in self.by_name.values(): - yield ext - - def get(self, name, default=missing): - if name in self.by_name: - return self.by_name[name] - - if name in self.by_attr: - return self.by_attr[name] - - if default is not missing: - return default - - def collect(self): - return dict(self.by_name) - - def register(self, extension): - '''Register an extension''' - - if not is_extension_type(extension): - raise RuntimeError('Not an extension: %s' % extension) - - if extension.name in _extensions: - _log.debug('Extension already loaded: %s' % extension) - return - - try: - instance = extension() - instance._load() - except Exception as e: - _log.debug( - 'Failed to load extension: %s: %s' % (extension, e.message) - ) - return - - self.by_name[extension.name] = instance - self.by_attr[extension.attr_name] = instance - _log.debug('Registered extension: %s', extension) - - def unregister(self, extension): - '''Unregister an extension''' - - if not is_extension_type(extension): - raise RuntimeError('Not an extension: %s' % extension) - - registered_extension = self.by_name.get(extension.name, None) - is_same_extension = ( - registered_extension is extension or - isinstance(registered_extension, extension) or - registered_extension.name == extension.name - ) - if is_same_extension: - self.by_name.pop(extension.name) - self.by_attr.pop(extension.attr_name) - registered_extension._unload() - _log.debug('Unregistered extension: %s', registered_extension) - - def clear(self): - '''Unloads and deletes all extensions''' - - while self.by_name: - name, ext = self.by_name.popitem() - ext.unload() - _log.debug('Unregistered extension: %s' % ext) - del ext - - self.by_attr = {} - - def discover(self, *paths): - '''Discover extensions - - 1. Search construct.extensions entry_point - 2. Search paths passed to discover_extensions - 3. Search configuration EXTENSION_PATHS - 4. Search CONSTRUCT_EXTENSION_PATHS environment variable - ''' - - from construct.api import config - - search_paths = list(paths) - - entry_points = pkg_resources.iter_entry_points(EXTENSIONS_ENTRY_POINT) - for entry_point in entry_points: - obj = entry_point.load() - for _, extension in inspect.getmembers(obj, is_extension_type): - self.register(extension) - - cfg_search_paths = config.get('EXTENSION_PATHS', []) - search_paths.extend(cfg_search_paths) - - env_search_paths = os.environ.get('CONSTRUCT_EXTENSION_PATHS') - if env_search_paths: - search_paths.extend(env_search_paths.split(os.pathsep)) - - for mod in iter_modules(*search_paths): - for _, extension in inspect.getmembers(mod, is_extension_type): - self.register(extension) - - -EXTENSION_TYPES = (Extension, HostExtension) - - -def is_extension_type(obj): - return ( - obj not in EXTENSION_TYPES and - isinstance(obj, type) and - issubclass(obj, EXTENSION_TYPES) - ) - - -def is_extension(obj): - return isinstance(obj, EXTENSION_TYPES) diff --git a/construct/extensions.py b/construct/extensions.py new file mode 100644 index 0000000..29604b8 --- /dev/null +++ b/construct/extensions.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import inspect +import logging + +# Third party imports +# Third library imports +import entrypoints + +# Local imports +from .constants import EXTENSIONS_ENTRY_POINT +from .utils import iter_modules + + +__all__ = [ + 'Extension', + 'ExtensionManager', + 'Host', + 'is_extension_type', + 'is_extension', +] + + +_log = logging.getLogger(__name__) + + +class Extension(object): + '''The Extension class allows users to extend Construct with their own + behavior and functionality. When writing your own Extensions use the + `setup` method instead of `__init__` to perform any setup your Extension + requires like creating a database connection. The `load` method should be + used to register event handlers and extend the base API. The `unload` + method should be used to undo everything that was done in `load`. + + Extensions commonly do the following: + + - Emit custom events + - Provide event handlers + - Provide Methods and Objects to extend the base API + + Look at construct.builtins to see the Extensions that provide the core + functionality of Construct. + ''' + + identifier = '' + label = '' + icon = '' + + def __init__(self, api=None): + self.api = api + + def setup(self, api): + '''Called by Construct prior to loading the extension. This is a good + place to set up dependencies like database connections. + ''' + + def load(self, api): + '''Called by Construct when loading the extension. + + This method should register event handlers and extend the base API. + ''' + + def unload(self, api): + '''Called by Construct when unloading the extension. + + This method should undo everything that was done in `load`. + ''' + + def is_available(self, ctx): + '''Return True if the Extension is available in the given context''' + return True + + +class Host(Extension): + '''An Extension providing a unified api to access the Host software that + Construct is operating within like, Autodesk Maya or SideFX Houdini. + The active Host Extension is bound to API.host. + ''' + + @property + def version(self): + '''Application version''' + return NotImplemented + + def modified(self): + '''Check if current file is modified''' + return NotImplemented + + def save_file(self, file): + '''Save a file''' + return NotImplemented + + def open_file(self, file): + '''Open a file''' + return NotImplemented + + def get_selection(self): + '''Get the host's selection''' + return NotImplemented + + def set_selection(self, selection): + '''Set the host's selection''' + return NotImplemented + + def get_workspace(self): + '''Get the host's workspace''' + return NotImplemented + + def set_workspace(self, directory): + '''Set the host's workspace''' + return NotImplemented + + def get_filepath(self): + '''Get the path to the host's current file''' + return NotImplemented + + def get_filename(self): + '''Get the name of the host's current file''' + return NotImplemented + + def get_frame_rate(self): + '''Get the frame rate of the host + Returns: + float - fps + ''' + return NotImplemented + + def set_frame_rate(self, fps): + '''Set the frame rate of the host + Arguments: + fps (float) - Frames per second + ''' + return NotImplemented + + def get_frame_range(self): + '''Get the frame range of the host + Returns: + [min_frame, start_frame, end_frame, max_frame] + ''' + return NotImplemented + + def set_frame_range(self, min, start, end, max): + '''Set the frame range of the host + Arguments: + min_frame (int): Min frame of timeline + start_frame (int): Start frame of animation + end_frame (int): End frame of animation + max_frame (int): Max frame of timeline + ''' + return NotImplemented + + def before_launch(self, api, software, env, ctx): + '''Called before the Host is launched. + + Used to provision environment variables and setup a workspace. + ''' + return NotImplemented + + def after_launch(self, api, ctx): + '''Called after application is launched within the Host application. + + Implementations should do some of the following: + + - Register callbacks + - Setup UI Menus + - Setup Shelves + - Setup UI Elements + - Launch a dialog + ''' + return NotImplemented + + def get_qt_parent(self): + '''Get the host's main QT widget''' + return NotImplemented + + def get_qt_app(self): + from Qt import QtWidgets + return QtWidgets.QApplication.instance() + + +EXTENSION_TYPES = (Extension, Host) + + +def is_extension_type(obj): + '''Check if an obj is an Extension type.''' + + return ( + obj not in EXTENSION_TYPES and + isinstance(obj, type) and + issubclass(obj, EXTENSION_TYPES) + ) + + +def is_extension(obj): + '''Check if an obj is an Extension instance.''' + + return isinstance(obj, EXTENSION_TYPES) + + +class ExtensionManager(dict): + + def __init__(self, api): + self.api = api + self.path = self.api.path + self.settings = self.api.settings + + def load(self): + '''Discover and load all Extensions''' + + for ext in self.discover(): + self.register(ext) + + def unload(self): + '''Unload all Extensions''' + + for ext in self.values(): + _log.debug('Unloading extension: %s' % ext) + ext.unload(self.api) + + self.clear() + + def register(self, ext): + '''Register an Extension''' + + if self.loaded(ext): + _log.debug('Extension already loaded: %s' % ext) + + _log.debug('Loading extension: %s' % ext) + if is_extension_type(ext): + inst = ext(self.api) + elif is_extension(ext): + inst = ext + else: + _log.error('Expected Extension type got %s' % ext) + return + + try: + inst.setup(self.api) + except: + _log.exception("Failed to setup extension: %s" % ext) + return + + try: + inst.load(self.api) + self[ext.identifier] = inst + except: + _log.exception("Failed to load extension: %s" % ext) + return + + def unregister(self, ext): + '''Unregister an Extension''' + + identifier = getattr(ext, 'identifier', ext) + inst = self.pop(identifier, None) + _log.debug('Unloading extension: %s' % ext) + try: + inst.unload(self.api) + except: + _log.exception("Failed to unload extension: %s" % ext) + return + + def loaded(self, ext): + '''Check if an Extension has been loaded.''' + + identifier = getattr(ext, 'identifer', ext) + return identifier in self + + def discover(self): + '''Find and iterate over all Extension subclasses + + 1. Yields Builtin Extensions + 2. Yields Extensions registered to construct.extensions entry_point + 2. Yields Extensions in python files in CONSTRUCT_PATH + 3. Yields Extensions in settings['extensions'] + ''' + + from .ext import extensions + for ext in extensions: + yield ext + + from .hosts import extensions + for ext in extensions: + yield ext + + entry_points = entrypoints.get_group_all(EXTENSIONS_ENTRY_POINT) + for entry_point in entry_points: + obj = entry_point.load() + for _, ext in inspect.getmembers(obj, is_extension_type): + yield ext + + ext_paths = [p / 'extensions' for p in self.path] + for mod in iter_modules(*ext_paths): + for _, ext in inspect.getmembers(mod, is_extension_type): + yield ext + + for module_path in self.settings.get('extensions', []): + try: + mod = __import__(module_path) + except ImportError: + _log.debug('Extension module does not exist: ' + module_path) + for _, ext in inspect.getmembers(mod, is_extension_type): + yield ext + + def ls(self, typ=None): + '''Get a list of available extensions. + + Arguments: + typ (Extension, Optional): List only a specific type of Extension. + + Examples: + ls() # lists all extensions + ls(Host) # list only Host extensions + ''' + + matches = [] + for ext in self.values(): + if not type or isinstance(ext, typ): + matches.append(ext) + return matches + + def get_available(self, ctx=None): + '''Get extensions available in the provided context.''' + + ctx = ctx or self.api.context.copy() + return [ext for ext in self.values() if ext.is_available(ctx)] diff --git a/construct/hosts/__init__.py b/construct/hosts/__init__.py new file mode 100644 index 0000000..dcf33f9 --- /dev/null +++ b/construct/hosts/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Local imports +from .hou.host import Houdini +from .maya.host import Maya +from .nuke.host import Nuke + + +extensions = [ + Houdini, + Maya, + Nuke, +] diff --git a/construct/hosts/hou/__init__.py b/construct/hosts/hou/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/construct/hosts/hou/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/construct/hosts/hou/callbacks.py b/construct/hosts/hou/callbacks.py new file mode 100644 index 0000000..c36c9aa --- /dev/null +++ b/construct/hosts/hou/callbacks.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function + +# Standard library imports +import logging + + +_log = logging.getLogger('construct.hou.callbacks') + + +def set_context_to_hou_scene(): + '''Sets the Context to the current houd scene, if it's in a workspace''' + _log.debug('Setting Construct context from Houdini Scene.') + + +def scene_event_callback(event_type): + '''Scene event callback''' + + import hou + + accepted_events = ( + hou.hipFileEventType.AfterLoad, + hou.hipFileEventType.AfterSave, + ) + if event_type in accepted_events: + set_context_to_hou_scene() + + +def register(): + '''Register construct_hou callbacks''' + + import hou + hou.hipFile.addEventCallback(scene_event_callback) + + +def unregister(): + '''Register construct_hou callbacks''' + + import hou + hou.hipFile.addEventCallback(scene_event_callback) diff --git a/construct/hosts/hou/host.py b/construct/hosts/hou/host.py new file mode 100644 index 0000000..98f4fd9 --- /dev/null +++ b/construct/hosts/hou/host.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +from collections import namedtuple +from os.path import basename, isfile + +# Local imports +from ...compat import Path +from ...extensions import Host +from ...utils import copy_file, update_env + + +__all__ = ['Houdini'] +package_path = Path(__file__).parent.resolve() +Version = namedtuple('Version', 'major minor patch') + + +class Houdini(Host): + '''Construct Houdini integration''' + + identifier = 'houdini' + label = 'SideFX Houdini' + icon = 'icons/houdini.png' + + def load(self, api): + api.path.append(package_path) + for software_config in package_path.glob('software/*.yaml'): + if software_config.stem not in api.software: + copy_file(software_config, api.software.folder) + + def unload(self, api): + pass + + @property + def version(self): + import hou + return Version(hou.applicationVersion()) + + def modified(self): + import hou + return ( + hou.hipFile.hasUnsavedChanges() and + isfile(self.get_filename()) + ) + + def save_file(self, file): + import hou + + hou.hipFile.save(file) + + def open_file(self, file): + import hou + from construct.ui.dialogs import ask + + if self.modified(): + if ask('Would you like to save?', title='Unsaved changes'): + hou.hipFile.save() + + hou.hipFile.load(file, suppress_save_prompt=True) + + def get_selection(self): + import hou + return hou.selectedNodes() + + def set_selection(self, selection): + for node in self.get_selection(): + node.setSelected(False) + for node in selection: + node.setSelected(True) + + def get_workspace(self): + import os + return os.environ['JOB'] + + def set_workspace(self, directory): + import os + os.environ['JOB'] = directory + + def get_filepath(self): + import hou + return hou.hipFile.path() + + def get_filename(self): + import hou + return basename(hou.hipFile.path()) + + def get_frame_range(self): + import hou + if self.version > 15: + min, max = hou.playbar.frameRange() + start, end = hou.playbar.playbackRange() + else: + min, max = hou.playbar.timelineRange() + start = min + end = max + return min, start, end, max + + def set_frame_range(self, min, start, end, max): + import hou + if self.version > 15: + hou.playbar.setFrameRange(min, max) + hou.playbar.setPlaybackRange(start, end) + else: + hou.setPlaybackRange(min, max) + + def get_frame_rate(self): + import hou + return hou.fps() + + def set_frame_rate(self, fps): + import hou + hou.setFps(fps) + + def get_qt_parent(self): + import hou + if self.version.major > 15: + return hou.qt.mainWindow() + else: + return hou.ui.mainQtWindow() + + def before_launch(self, api, software, env, ctx): + + startup_path = (package_path / 'startup').as_posix() + update_env( + env, + HOUDINI_PATH=[startup_path, '&'], + ) + + def after_launch(self, api, ctx): + from . import callbacks + callbacks.register() diff --git a/construct/hosts/hou/icons/houdini.png b/construct/hosts/hou/icons/houdini.png new file mode 100644 index 0000000..9811248 Binary files /dev/null and b/construct/hosts/hou/icons/houdini.png differ diff --git a/construct/hosts/hou/software/houdini.yaml b/construct/hosts/hou/software/houdini.yaml new file mode 100644 index 0000000..9c195d1 --- /dev/null +++ b/construct/hosts/hou/software/houdini.yaml @@ -0,0 +1,21 @@ +schema_version: '0.1.0' +label: 'Houdini 17.5' +icon: 'icons/houdini.png' +host: 'houdini' +cmd: + linux: + - '/opt/hfs17.5.327/bin/houdini' + - '/opt/hfs17.5.293/bin/houdini' + - '/opt/hfs17.5.173/bin/houdini' + mac: + - '/Applications/Houdini 17.5.327/Houdini.app' + - '/Applications/Houdini 17.5.293/Houdini.app' + - '/Applications/Houdini 17.5.173/Houdini.app' + win: + - 'C:/Program Files/Side Effects Software/Houdini 17.5.327/bin/houdinifx.exe' + - 'C:/Program Files/Side Effects Software/Houdini 17.5.293/bin/houdinifx.exe' + - 'C:/Program Files/Side Effects Software/Houdini 17.5.173/bin/houdinifx.exe' +args: [] +env: {} +template: 'templates/houdini_default' +extensions: ['.hip', '.hipnc'] diff --git a/construct/hosts/hou/startup/MainMenuCommon.xml b/construct/hosts/hou/startup/MainMenuCommon.xml new file mode 100644 index 0000000..b6602d3 --- /dev/null +++ b/construct/hosts/hou/startup/MainMenuCommon.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/construct/hosts/hou/startup/python2.7libs/pythonrc.py b/construct/hosts/hou/startup/python2.7libs/pythonrc.py new file mode 100644 index 0000000..412256c --- /dev/null +++ b/construct/hosts/hou/startup/python2.7libs/pythonrc.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + +# Third party imports +from hdefereval import executeDeferred + +# Local imports +import construct + + +_log = logging.getLogger('construct.hou.pythonrc') + + +def load_construct(): + api = construct.API() + _log.debug('Construct loaded in SideFX Houdini.') + api.host.after_launch(api, api.context.copy()) + + +executeDeferred(load_construct) diff --git a/construct/builtins/templates/vfx_asset/.data/tag_asset b/construct/hosts/hou/templates/houdini_default/abc/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_asset/.data/tag_asset rename to construct/hosts/hou/templates/houdini_default/abc/.gitkeep diff --git a/construct/builtins/templates/vfx_collection/.data/data b/construct/hosts/hou/templates/houdini_default/audio/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_collection/.data/data rename to construct/hosts/hou/templates/houdini_default/audio/.gitkeep diff --git a/construct/builtins/templates/vfx_collection/.data/tag_collection b/construct/hosts/hou/templates/houdini_default/comp/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_collection/.data/tag_collection rename to construct/hosts/hou/templates/houdini_default/comp/.gitkeep diff --git a/construct/builtins/templates/vfx_project/.data/tag_project b/construct/hosts/hou/templates/houdini_default/desk/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/.data/tag_project rename to construct/hosts/hou/templates/houdini_default/desk/.gitkeep diff --git a/construct/builtins/templates/vfx_project/.data/uuid_69c45613-75a9-4b90-aaa5-d40323214e58 b/construct/hosts/hou/templates/houdini_default/flip/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/.data/uuid_69c45613-75a9-4b90-aaa5-d40323214e58 rename to construct/hosts/hou/templates/houdini_default/flip/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/.data/data b/construct/hosts/hou/templates/houdini_default/geo/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/.data/data rename to construct/hosts/hou/templates/houdini_default/geo/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/.data/tag_collection b/construct/hosts/hou/templates/houdini_default/hda/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/.data/tag_collection rename to construct/hosts/hou/templates/houdini_default/hda/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/character/.data/data b/construct/hosts/hou/templates/houdini_default/render/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/character/.data/data rename to construct/hosts/hou/templates/houdini_default/render/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/character/.data/tag_asset_type b/construct/hosts/hou/templates/houdini_default/scripts/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/character/.data/tag_asset_type rename to construct/hosts/hou/templates/houdini_default/scripts/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/character/.data/uuid_e71f7370-7e40-40e5-ab90-7c005677427e b/construct/hosts/hou/templates/houdini_default/sim/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/character/.data/uuid_e71f7370-7e40-40e5-ab90-7c005677427e rename to construct/hosts/hou/templates/houdini_default/sim/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/environment/.data/data b/construct/hosts/hou/templates/houdini_default/tex/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/environment/.data/data rename to construct/hosts/hou/templates/houdini_default/tex/.gitkeep diff --git a/construct/builtins/templates/vfx_project/assets/environment/.data/tag_asset_type b/construct/hosts/hou/templates/houdini_default/video/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/environment/.data/tag_asset_type rename to construct/hosts/hou/templates/houdini_default/video/.gitkeep diff --git a/construct/hosts/maya/__init__.py b/construct/hosts/maya/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/construct/hosts/maya/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/construct/hosts/maya/callbacks.py b/construct/hosts/maya/callbacks.py new file mode 100644 index 0000000..1dd6e29 --- /dev/null +++ b/construct/hosts/maya/callbacks.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, print_function + +# Standard library imports +import logging + + +_log = logging.getLogger('construct.maya.callbacks') +_callback_ids = [] + + +def after_open(*args): + '''kAfterOpen callback''' + + _log.debug('after_open') + # TODO: Set the construct context and maya workspace based on the + # opened file. + + +def after_save(*args): + '''kAfterSave callback''' + + _log.debug('after_open') + # TODO: Set the construct context and maya workspace based on the + # saved file. + + +def before_create_reference_check(mfile, client_data): + '''kBeforeCreateReferenceCheck callback''' + + _log.debug('before_create_reference_check') + # TODO: make sure that the referenced file is the latest version + # of a publish. If it is not, prompt user to update. + return True + + +def register(): + '''Register scene callbacks''' + + from maya.api import OpenMaya + MSceneMessage = OpenMaya.MSceneMessage + + after_open_id = MSceneMessage.addCallback( + MSceneMessage.kAfterOpen, + after_open + ) + _callback_ids.append(after_open_id) + + after_save_id = MSceneMessage.addCallback( + MSceneMessage.kAfterSave, + after_save + ) + _callback_ids.append(after_save_id) + + before_create_reference_check_id = MSceneMessage.addCheckFileCallback( + MSceneMessage.kBeforeCreateReferenceCheck, + before_create_reference_check + ) + _callback_ids.append(before_create_reference_check_id) + + +def unregister(): + '''Unregister scene callbacks''' + + from maya.api import OpenMaya + + while _callback_ids: + _id = _callback_ids.pop() + OpenMaya.MMessage.removeCallback(_id) diff --git a/construct/hosts/maya/host.py b/construct/hosts/maya/host.py new file mode 100644 index 0000000..da63594 --- /dev/null +++ b/construct/hosts/maya/host.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +from collections import namedtuple +from os.path import basename + +# Local imports +from ...compat import Path +from ...extensions import Host +from ...utils import copy_file, update_env + + +__all__ = ['Maya'] +package_path = Path(__file__).parent.resolve() +Version = namedtuple('Version', 'major minor patch') + + +class Maya(Host): + '''Construct Autodesk Maya integration''' + + identifier = 'maya' + label = 'Autodesk Maya' + icon = 'icons/maya.png' + + def load(self, api): + api.path.append(package_path) + for software_config in package_path.glob('software/*.yaml'): + if software_config.stem not in api.software: + copy_file(software_config, api.software.folder) + + def unload(self, api): + pass + + @property + def version(self): + from maya import cmds + major = str(cmds.about(version=True)) + apiver = str(cmds.about(apiVersion=True)).replace(major, '') + minor = apiver[:2] + patch = apiver[2:] + return Version(int(major), int(minor), int(patch)) + + def modified(self): + from maya import cmds + + return ( + cmds.file(query=True, modified=True) and + self.get_filename() + ) + + def save_file(self, file): + from maya import cmds + + cmds.file(rename=file) + cmds.file(save=True) + + def open_file(self, file): + from maya import cmds + from construct.ui.dialogs import ask + + if self.modified(): + if ask('Would you like to save?', title='Unsaved changes'): + cmds.file(save=True, force=True) + + cmds.file(new=True, force=True) + cmds.file(file, open=True, ignoreVersion=True) + + def get_selection(self): + from maya import cmds + + return cmds.ls(selection=True, long=True) + + def set_selection(self, selection): + from maya import cmds + + cmds.select(selection, replace=True) + + def get_workspace(self): + from maya import cmds + + return cmds.workspace(query=True, openWorkspace=True) + + def set_workspace(self, directory): + from maya import cmds + + cmds.workspace(directory, openWorkspace=True) + + def get_filepath(self): + from maya import cmds + + return cmds.file(query=True, sceneName=True) + + def get_filename(self): + from maya import cmds + + return basename(cmds.file(query=True, sceneName=True)) + + def get_frame_rate(self): + from maya import cmds + + unit = cmds.currentUnit(query=True, time=True) + fps = { + 'game': '15fps', + 'film': '24fps', + 'pal': '25fps', + 'ntsc': '30fps', + 'show': '48fps', + 'palf': '50fps', + 'ntscf': '60fps', + }.get(unit, unit) + return float(fps.rstrip('fps')) + + def set_frame_rate(self, fps): + from maya import cmds + + whole, decimal = str(float(fps)).split('.') + if int(decimal) == 0: + fps = whole + 'fps' + else: + fps = str(fps) + 'fps' + unit = { + 'game': '15fps', + 'film': '24fps', + 'pal': '25fps', + 'ntsc': '30fps', + 'show': '48fps', + 'palf': '50fps', + 'ntscf': '60fps', + }.get(fps, fps) + cmds.currentUnit(time=unit) + + def get_frame_range(self): + from maya import cmds + + return [ + cmds.playbackOptions(query=True, animationStartTime=True), + cmds.playbackOptions(query=True, minTime=True), + cmds.playbackOptions(query=True, maxTime=True), + cmds.playbackOptions(query=True, animationEndTime=True), + ] + + def set_frame_range(self, min, start, end, max): + from maya import cmds + + cmds.playbackOptions( + animationStartTime=min, + minTime=start, + maxTime=end, + animationEndTime=max, + ) + + def get_qt_parent(self): + from Qt import QtWidgets + app = QtWidgets.QApplication.instance() + + for widget in app.topLevelWidgets(): + if widget.objectName() == 'MayaWindow': + return widget + + def before_launch(self, api, software, env, ctx): + + startup_path = (package_path / 'startup').as_posix() + update_env( + env, + MAYA_SCRIPT_PATH=[startup_path], + PYTHONPATH=[startup_path], + ) + + def after_launch(self, api, ctx): + from . import callbacks + callbacks.register() diff --git a/construct/hosts/maya/icons/maya.png b/construct/hosts/maya/icons/maya.png new file mode 100644 index 0000000..d6736ad Binary files /dev/null and b/construct/hosts/maya/icons/maya.png differ diff --git a/construct/hosts/maya/icons/mayapy.png b/construct/hosts/maya/icons/mayapy.png new file mode 100644 index 0000000..3b790d9 Binary files /dev/null and b/construct/hosts/maya/icons/mayapy.png differ diff --git a/construct/hosts/maya/software/maya.yaml b/construct/hosts/maya/software/maya.yaml new file mode 100644 index 0000000..7322956 --- /dev/null +++ b/construct/hosts/maya/software/maya.yaml @@ -0,0 +1,12 @@ +schema_version: '0.1.0' +label: 'Maya 2019' +icon: 'icons/maya.png' +host: 'maya' +cmd: + linux: '/usr/autodesk/maya2019/bin/maya' + mac: '/Applications/Autodesk/maya2019/bin/maya' + win: 'C:/Program Files/Autodesk/Maya2019/bin/maya.exe' +args: [-noAutoloadPlugins] +env: {} +template: 'templates/maya_default' +extensions: ['.mb', '.ma', '.obj', '.abc', '.fbx', '.usd'] diff --git a/construct/hosts/maya/software/mayapy.yaml b/construct/hosts/maya/software/mayapy.yaml new file mode 100644 index 0000000..b22de68 --- /dev/null +++ b/construct/hosts/maya/software/mayapy.yaml @@ -0,0 +1,12 @@ +schema_version: '0.1.0' +label: 'MayaPy 2019' +icon: 'icons/mayapy.png' +host: 'maya' +cmd: + linux: '/usr/autodesk/maya2019/bin/mayapy' + mac: '/Applications/Autodesk/maya2019/bin/mayapy' + win: 'C:/Program Files/Autodesk/Maya2019/bin/mayapy.exe' +args: [] +env: {} +template: 'templates/maya_default' +extensions: ['.mb', '.ma', '.obj', '.abc', '.fbx', '.usd'] diff --git a/construct/hosts/maya/startup/userSetup.py b/construct/hosts/maya/startup/userSetup.py new file mode 100644 index 0000000..fa7e315 --- /dev/null +++ b/construct/hosts/maya/startup/userSetup.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + +# Third party imports +from maya.utils import executeDeferred + +# Local imports +import construct + + +_log = logging.getLogger('construct.maya.userSetup') + + +def load_construct(): + _log.debug('Construct loaded in Autodesk Maya.') + api = construct.API() + api.host.after_launch(api, api.context.copy()) + + +executeDeferred(load_construct) diff --git a/construct/hosts/maya/templates/maya_default/workspace.mel b/construct/hosts/maya/templates/maya_default/workspace.mel new file mode 100644 index 0000000..344b65d --- /dev/null +++ b/construct/hosts/maya/templates/maya_default/workspace.mel @@ -0,0 +1,39 @@ +//Maya 2019 Project Definition + +workspace -fr "3dPaintTextures" "sourceimages/3dPaintTextures"; +workspace -fr "Alembic" "cache/alembic"; +workspace -fr "audio" "sound"; +workspace -fr "autoSave" "autosave"; +workspace -fr "clips" "clips"; +workspace -fr "depth" "renderData/depth"; +workspace -fr "diskCache" "data"; +workspace -fr "eps" "data"; +workspace -fr "fileCache" "cache/nCache"; +workspace -fr "fluidCache" "cache/nCache/fluid"; +workspace -fr "furAttrMap" "renderData/fur/furAttrMap"; +workspace -fr "furEqualMap" "renderData/fur/furEqualMap"; +workspace -fr "furFiles" "renderData/fur/furFiles"; +workspace -fr "furImages" "renderData/fur/furImages"; +workspace -fr "furShadowMap" "renderData/fur/furShadowMap"; +workspace -fr "illustrator" "data"; +workspace -fr "images" "images"; +workspace -fr "iprImages" "renderData/iprImages"; +workspace -fr "mayaAscii" ""; +workspace -fr "mayaBinary" ""; +workspace -fr "mel" "scripts"; +workspace -fr "move" "data"; +workspace -fr "movie" "movies"; +workspace -fr "OBJ" "data"; +workspace -fr "offlineEdit" "edits"; +workspace -fr "particles" "cache/particles"; +workspace -fr "renderData" "renderData"; +workspace -fr "scene" ""; +workspace -fr "sceneAssembly" "sceneAssembly"; +workspace -fr "scripts" "scripts"; +workspace -fr "shaders" "renderData/shaders"; +workspace -fr "sound" "sound"; +workspace -fr "sourceImages" "sourceimages"; +workspace -fr "teClipExports" "Time Editor/Clip Exports"; +workspace -fr "templates" "assets"; +workspace -fr "timeEditor" "Time Editor"; +workspace -fr "translatorData" "data"; diff --git a/construct/hosts/nuke/__init__.py b/construct/hosts/nuke/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/construct/hosts/nuke/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/construct/hosts/nuke/callbacks.py b/construct/hosts/nuke/callbacks.py new file mode 100644 index 0000000..c3944fc --- /dev/null +++ b/construct/hosts/nuke/callbacks.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + + +_log = logging.getLogger('construct.nuke.callbacks') + + +def on_script_save(): + '''onScriptSave callback''' + + import nuke + _log.debug('on_script_save') + + +def on_script_load(): + '''onScriptLoad callback''' + + import nuke + _log.debug('on_script_load') + + +def register(): + '''Register script callbacks''' + + import nuke + nuke.addOnScriptLoad(on_script_load) + nuke.addOnScriptSave(on_script_save) + + +def unregister(): + '''Unregister script callbacks''' + + import nuke + nuke.removeOnScriptLoad(on_script_load) + nuke.removeOnScriptSave(on_script_save) diff --git a/construct/hosts/nuke/host.py b/construct/hosts/nuke/host.py new file mode 100644 index 0000000..3435c46 --- /dev/null +++ b/construct/hosts/nuke/host.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import os +from collections import namedtuple +from os.path import basename + +# Local imports +from ...compat import Path +from ...extensions import Host +from ...utils import copy_file, update_env + + +__all__ = ['Nuke'] +package_path = Path(__file__).parent.resolve() +Version = namedtuple('Version', 'major minor patch') + + +class Nuke(Host): + '''Construct Nuke Host Extension''' + + identifier = 'nuke' + label = 'The Foundry Nuke' + icon = 'icons/nuke.png' + + def load(self, api): + api.path.append(package_path) + for software_config in package_path.glob('software/*.yaml'): + if software_config.stem not in api.software: + copy_file(software_config, api.software.folder) + + def unload(self, api): + pass + + @property + def version(self): + import nuke + major = nuke.NUKE_VERSION_MAJOR + minor = nuke.NUKE_VERSION_MINOR + patch = nuke.NUKE_VERSION_RELEASE + return Version(major, minor, patch) + + def modified(self): + import nuke + return ( + nuke.root().modified() and + self.get_filename() != 'Root' + ) + + def save_file(self, file): + import nuke + nuke.scriptSaveAs(file) + + def open_file(self, file): + import nuke + nuke.scriptOpen(file) + + def get_selection(self): + import nuke + return nuke.selectedNodes() + + def set_selection(self, selection): + for node in self.get_selection(): + node.setSelected(False) + for node in selection: + node.setSelected(True) + + def get_workspace(self): + import nuke + return nuke.root()['project_directory'].getValue() + + def set_workspace(self, directory): + import nuke + + fav_icon = self.api.ui.resources.get( + 'brand/construct_icon-white-on-black.png' + ) + nuke.removeFavoriteDir('project') + nuke.removeFavoriteDir('asset') + nuke.removeFavoriteDir('workspace') + nuke.addFavoriteDir( + 'workspace', + directory=directory, + type=(nuke.IMAGE | nuke.SCRIPT | nuke.GEO), + icon=fav_icon, + tooltip=directory + ) + + os.chdir(directory) + nuke.root()['project_directory'].setValue(directory) + + def get_filepath(self): + import nuke + return nuke.root().name() + + def get_filename(self): + import nuke + return basename(nuke.root().name()) + + def get_frame_range(self): + import nuke + viewer = nuke.activeViewer().node() + viewer_range = viewer.knob('frame_range').getValue() + start, end = [int(v) for v in viewer_range.split('-')] + root = nuke.root() + min = root.firstFrame() + max = root.lastFrame() + return min, start, end, max + + def set_frame_range(self, min, start, end, max): + import nuke + root = nuke.root() + root.knob('first_frame').setValue(min) + root.knob('last_frame').setValue(max) + viewer = nuke.activeViewer().node() + viewer['frame_range'].setValue('%d-%d' % (start, end)) + viewer['frame_range_lock'].setValue(True) + + def get_frame_rate(self): + import nuke + root = nuke.root() + return root.knob('fps').getValue() + + def set_frame_rate(self, fps): + import nuke + root = nuke.root() + return root.knob('fps').setValue(fps) + + def get_qt_parent(self): + from Qt import QtWidgets + app = QtWidgets.QApplication.instance() + + for widget in app.topLevelWidgets(): + if isinstance(widget, QtWidgets.QMainWindow): + return widget + + def before_launch(self, api, software, env, ctx): + startup_path = (package_path / 'startup').as_posix() + update_env( + env, + NUKE_PATH=[startup_path], + ) + + def after_launch(self, api, ctx): + from . import callbacks + callbacks.register() diff --git a/construct/hosts/nuke/icons/nuke.png b/construct/hosts/nuke/icons/nuke.png new file mode 100644 index 0000000..db408b7 Binary files /dev/null and b/construct/hosts/nuke/icons/nuke.png differ diff --git a/construct/hosts/nuke/icons/nukex.png b/construct/hosts/nuke/icons/nukex.png new file mode 100644 index 0000000..11918dd Binary files /dev/null and b/construct/hosts/nuke/icons/nukex.png differ diff --git a/construct/hosts/nuke/software/nuke.yaml b/construct/hosts/nuke/software/nuke.yaml new file mode 100644 index 0000000..d5e3b3a --- /dev/null +++ b/construct/hosts/nuke/software/nuke.yaml @@ -0,0 +1,24 @@ +schema_version: '0.1.0' +label: 'Nuke 11.3' +icon: 'icons/nuke.png' +host: 'nuke' +cmd: + linux: + - /usr/local/Nuke11.3v4/Nuke11.3 + - /usr/local/Nuke11.3v3/Nuke11.3 + - /usr/local/Nuke11.3v2/Nuke11.3 + - /usr/local/Nuke11.3v1/Nuke11.3 + mac: + - /Applications/Nuke11.3v4/Nuke11.3v4.app/Nuke11.3 + - /Applications/Nuke11.3v3/Nuke11.3v3.app/Nuke11.3 + - /Applications/Nuke11.3v2/Nuke11.3v2.app/Nuke11.3 + - /Applications/Nuke11.3v1/Nuke11.3v1.app/Nuke11.3 + win: + - C:/Program Files/Nuke11.3v4/Nuke11.3.exe + - C:/Program Files/Nuke11.3v3/Nuke11.3.exe + - C:/Program Files/Nuke11.3v2/Nuke11.3.exe + - C:/Program Files/Nuke11.3v1/Nuke11.3.exe +args: [] +env: {} +template: 'templates/nuke_default' +extensions: ['.nk'] diff --git a/construct/hosts/nuke/software/nukex.yaml b/construct/hosts/nuke/software/nukex.yaml new file mode 100644 index 0000000..cf836ed --- /dev/null +++ b/construct/hosts/nuke/software/nukex.yaml @@ -0,0 +1,24 @@ +schema_version: '0.1.0' +label: 'NukeX 11.3' +icon: 'icons/nuke.png' +host: 'nuke' +cmd: + linux: + - /usr/local/Nuke11.3v4/Nuke11.3 + - /usr/local/Nuke11.3v3/Nuke11.3 + - /usr/local/Nuke11.3v2/Nuke11.3 + - /usr/local/Nuke11.3v1/Nuke11.3 + mac: + - /Applications/Nuke11.3v4/Nuke11.3v4.app/Nuke11.3 + - /Applications/Nuke11.3v3/Nuke11.3v3.app/Nuke11.3 + - /Applications/Nuke11.3v2/Nuke11.3v2.app/Nuke11.3 + - /Applications/Nuke11.3v1/Nuke11.3v1.app/Nuke11.3 + win: + - C:/Program Files/Nuke11.3v4/Nuke11.3.exe + - C:/Program Files/Nuke11.3v3/Nuke11.3.exe + - C:/Program Files/Nuke11.3v2/Nuke11.3.exe + - C:/Program Files/Nuke11.3v1/Nuke11.3.exe +args: ['-nukex'] +env: {} +template: 'templates/nuke_default' +extensions: ['.nk'] diff --git a/construct/hosts/nuke/startup/init.py b/construct/hosts/nuke/startup/init.py new file mode 100644 index 0000000..643cf31 --- /dev/null +++ b/construct/hosts/nuke/startup/init.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + +# Local imports +import construct + + +_log = logging.getLogger('construct.nuke.init') +_log.debug('Initializing Nuke.') +api = construct.API() diff --git a/construct/hosts/nuke/startup/menu.py b/construct/hosts/nuke/startup/menu.py new file mode 100644 index 0000000..41c7d1b --- /dev/null +++ b/construct/hosts/nuke/startup/menu.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + +# Third party imports +from nukescripts.utils import executeDeferred + +# Local imports +import construct + + +_log = logging.getLogger('construct.nuke.menu') + + +def load_construct(): + api = construct.API() + _log.debug('Construct loaded in The Foundry Nuke.') + api.host.after_launch(api, api.context.copy()) + + +executeDeferred(load_construct) diff --git a/construct/builtins/templates/vfx_project/assets/environment/.data/uuid_4ead44a5-3d8b-4b44-a918-fde9318de284 b/construct/hosts/nuke/templates/nuke_default/images/.gitkeep similarity index 100% rename from construct/builtins/templates/vfx_project/assets/environment/.data/uuid_4ead44a5-3d8b-4b44-a918-fde9318de284 rename to construct/hosts/nuke/templates/nuke_default/images/.gitkeep diff --git a/construct/io/__init__.py b/construct/io/__init__.py new file mode 100644 index 0000000..9d9a14b --- /dev/null +++ b/construct/io/__init__.py @@ -0,0 +1,503 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +from copy import deepcopy + +# Local imports +from ..errors import ValidationError +from .fsfslayer import FsfsLayer + + +class IO(object): + + def __init__(self, api): + self.api = api + self.settings = self.api.settings + self.fsfs = FsfsLayer(api) + + def load(self): + self.api.define( + 'before_new_project', + '(api, project): Sent before a project is created.' + ) + self.api.define( + 'after_new_project', + '(api, project): Sent after a project is created.' + ) + self.api.define( + 'before_update_project', + '(api, project, update): Sent before a project is updated.' + ) + self.api.define( + 'after_update_project', + '(api, project): Set after a project is updated.' + ) + self.api.define( + 'before_new_folder', + '(api, folder): Sent before a folder is created.' + ) + self.api.define( + 'after_new_folder', + '(api, folder): Sent after a folder is created.' + ) + self.api.define( + 'before_update_folder', + '(api, folder, update): Sent before a folder is updated.' + ) + self.api.define( + 'after_update_folder', + '(api, folder): Set after a folder is updated.' + ) + + def unload(self): + self.api.undefine('before_new_project') + self.api.undefine('after_new_project') + self.api.undefine('before_update_project') + self.api.undefine('after_update_project') + self.api.undefine('before_new_folder') + self.api.undefine('after_new_folder') + self.api.undefine('before_update_folder') + self.api.undefine('after_update_folder') + + def get_projects(self, location=None, mount=None): + '''Get a list of projects in a location and mount. If no mount is + provided list projects in all mounts of the given location. + + .. note:: Locations and mounts are configured in your construct.yaml + + Arguments: + location (str): Location of projects. Default: my_location setting + mount (str): Mount project resides in. Optional. + + Returns: + Generator or cursor yielding projects + ''' + + location = location or self.api.context.location + + return self.fsfs.get_projects(location, mount) + + def get_project_by_id(self, _id, location=None, mount=None): + '''Get one project that matches the given name. You can specify a + location and mount to narrow the search. + + .. note:: Locations and mounts are configured in your construct.yaml + + Arguments: + _id (str): Project id + location (str): Location of projects. Default: my_location setting + mount (str): Mount project resides in. Optional. + + Returns: + Project dict or None + ''' + + location = location or self.api.context.location + + return self.fsfs.get_project_by_id(_id, location, mount) + + def get_project(self, name, location=None, mount=None): + '''Get one project that matches the given name. You can specify a + location and mount to narrow the search. + + .. note:: Locations and mounts are configured in your construct.yaml + + Arguments: + name (str): Partial name of project. + location (str): Location of projects. Default: my_location setting + mount (str): Mount project resides in. Optional. + + Returns: + Project dict or None + ''' + + location = location or self.api.context.location + + return self.fsfs.get_project(name, location, mount) + + def new_project(self, name, location, mount, data=None): + '''Create a new project in the specified location and mount. + + .. note:: Locations and mounts are configured in your construct.yaml + + Events: + before_new_project: Sent with api and project data + after_new_project: Sent with api and result project data + + Arguments: + name (str): Full name of new project. + location (str): Location of new project. + mount (str): Mount within the specified location. + + Returns: + New Project dict. + ''' + + # Prepare data + data = data or {} + data.setdefault('name', name) + + self.api.send('before_new_project', self.api, data) + + # Validate data + v = self.api.schemas.get_validator('project', allow_unknown=True) + data = v.validated(data) + if not data: + raise ValidationError( + 'Invalid project data: %s' % v.errors, + errors=v.errors + ) + + # Create our new project + result = self.fsfs.new_project(name, location, mount, data) + + self.api.send('after_new_project', self.api, result) + return result + + def update_project(self, project, data): + '''Update a project's data. + + Events: + before_update_project: Sent with api, project, and update + after_update_project: Sent with api, updated project + + Arguments: + project (dict): Project data containing at least a name + data (dict): Data to update + + Returns: + Updated project data. + ''' + + self.api.send('before_update_project', self.api, project, data) + + # Update and validate data + updated = deepcopy(project) + updated.update(data) + + v = self.api.schemas.get_validator('project', allow_unknown=True) + updated = v.validated(updated, update=True) + if not updated: + raise ValidationError( + 'Invalid project data: %s' % v.errors, + errors=v.errors + ) + + # Update our project + result = self.fsfs.update_project(project, updated) + self.api.send('after_update_project', self.api, result) + return result + + def delete_project(self, project): + '''Delete a project. + + .. warning:: This does not delete your project data on the file system. + It simply removes metadata. You are in charge of deleting on your + file system. + ''' + + return self.fsfs.delete_project(project) + + def get_assets(self, project, bin=None, asset_type=None, group=None): + '''Get all the assets in the specified project. + + Arguments: + project (dict): Project dict including _id + bin (str): Optional bin filter + asset_type (str): Optional asset_type filter + group (str): Optional group filter + + Returns: + Generator or cursor yielding assets + ''' + + return self.fsfs.get_assets(project, bin, asset_type, group) + + def get_asset(self, project, name): + '''Get one asset that matches the given name. + + Arguments: + project (dict): Project dict including _id + name (str): Partial name of asset. + + Returns: + Asset dict or None + ''' + + return self.fsfs.get_asset(project, name) + + def new_asset(self, project, name, bin, asset_type, data=None): + '''Create a new asset in the specified project. + + Events: + before_new_asset: Sent with api and asset data + after_new_asset: Sent with api and result asset data + + Arguments: + project (dict): Project dict including _id + name (str): Full name of new asset. + bin (str): Name of project bin + asset_type (str): Type of asset (asset, shot, etc.) + + Returns: + New asset dict. + ''' + + assert '_id' in project, 'project dict must include _id' + assert project['_type'] == 'project', 'project _type must be "project"' + + # Retrieve existing assets + project = self.get_project_by_id(project['_id']) + assets = project.get('assets', {}) + + if name in assets: + raise OSError('Asset already exists: %s' % name) + + # Prepare data + data = data or {} + data['name'] = name + data['bin'] = bin + data['asset_type'] = asset_type + data['project_id'] = project['_id'] + + self.api.send('before_new_asset', self.api, data) + + # Validate data + v = self.api.schemas.get_validator('asset', allow_unknown=True) + data = v.validated(data) + if not data: + raise ValidationError( + 'Invalid asset data: %s' % v.errors, + errors=v.errors + ) + + # Create our new asset + result = self.fsfs.new_asset(project, data) + + self.api.send('after_new_asset', self.api, result) + + assets[result['name']] = { + k: result[k] + for k in ['_id', 'name', 'bin', 'asset_type', 'group'] + } + self.update_project(project, {'assets': assets}) + + return result + + def update_asset(self, asset, data): + '''Update an asset's data. + + Events: + before_update_asset: Sent with api, asset, and update + after_update_asset: Sent with api, updated asset + + Arguments: + asset (dict): asset data containing at least a name + data (dict): Data to update + + Returns: + Updated asset data. + ''' + + self.api.send('before_update_asset', self.api, asset, data) + + # Update and validate data + updated = deepcopy(asset) + updated.update(data) + + v = self.api.schemas.get_validator('asset', allow_unknown=True) + updated = v.validated(updated, update=True) + if not updated: + raise ValidationError( + 'Invalid asset data: %s' % v.errors, + errors=v.errors + ) + + # Update our project + result = self.fsfs.update_asset(asset, updated) + self.api.send('after_update_asset', self.api, result) + return result + + def delete_asset(self, asset): + '''Delete an asset. + + .. warning:: This does not delete your asset data on the file system. + It simply removes metadata. You are in charge of deleting on your + file system. + ''' + + return self.fsfs.delete_asset(asset) + + def get_bins(self, project): + if 'bins' not in project: + project = self.get_project_by_id(project['_id']) + + return project.get('bins', {}) + + def get_bin(self, project, name): + if 'bins' not in project: + project = self.get_project_by_id(project['_id']) + + return project['bins'][name] + + def new_bin(self, project, name, data=None): + + # Retrieve existing project bins + project = self.get_project_by_id(project['_id']) + bins = project.get('bins', {}) + + # Prepare data + data = data or {} + data['name'] = name + data.setdefault('order', len(bins)) + data.setdefault('icon', '') + + # Update project with new bin + bins[name] = data + updated = self.update_project(project, {'bins': bins}) + + return updated['bins'][name] + + def update_bin(self, project, name, data): + + # Retrieve existing project bins + project = self.get_project_by_id(project['_id']) + bins = project.get('bins', {}) + + new_data = bins.pop(name) + new_data.update(data) + + # Update project with new bins + bins[new_data['name']] = new_data + updated = self.update_project(project, {'bins': bins}) + + return updated['bins'][new_data['name']] + + def delete_bin(self, project, name): + + # Retrieve existing project bins + project = self.get_project_by_id(project['_id']) + bins = project.get('bins', {}) + + self.update_project(project, {'bins': bins}) + + def get_asset_types(self, project): + if 'asset_types' not in project: + project = self.get_project_by_id(project['_id']) + + return project.get('asset_types', {}) + + def get_asset_type(self, project, name): + if 'asset_types' not in project: + project = self.get_project_by_id(project['_id']) + + return project['asset_types'][name] + + def new_asset_type(self, project, name, data=None): + + # Retrieve existing project asset_types + project = self.get_project_by_id(project['_id']) + asset_types = project.get('asset_types', {}) + + # Prepare data + data = data or {} + data['name'] = name + data.setdefault('order', len(asset_types)) + data.setdefault('icon', '') + + # Update project with new asset_type + asset_types[name] = data + updated = self.update_project(project, {'asset_types': asset_types}) + + return updated['asset_types'][name] + + def update_asset_type(self, project, name, data): + + # Retrieve existing project asset_types + project = self.get_project_by_id(project['_id']) + asset_types = project.get('asset_types', {}) + + new_data = asset_types.pop(name) + new_data.update(data) + + # Update project with new asset_types + asset_types[new_data['name']] = new_data + updated = self.update_project(project, {'asset_types': asset_types}) + + return updated['asset_types'][new_data['name']] + + def delete_asset_type(self, project, name): + + # Retrieve existing project asset_types + project = self.get_project_by_id(project['_id']) + asset_types = project.get('asset_types', {}) + + self.update_project(project, {'asset_types': asset_types}) + + def get_task_types(self, project): + if 'task_types' not in project: + project = self.get_project_by_id(project['_id']) + + return project.get('task_types', {}) + + def get_task_type(self, project, name): + if 'task_types' not in project: + project = self.get_project_by_id(project['_id']) + + return project['task_types'][name] + + def new_task_type(self, project, name, short, data=None): + + # Retrieve existing project task_types + project = self.get_project_by_id(project['_id']) + task_types = project.get('task_types', {}) + + # Prepare data + data = data or {} + data['name'] = name + data['short'] = short + data.setdefault('order', len(task_types)) + data.setdefault('icon', '') + + # Update project with new task_type + task_types[name] = data + updated = self.update_project(project, {'task_types': task_types}) + + return updated['task_types'][name] + + def update_task_type(self, project, name, data): + + # Retrieve existing project task_types + project = self.get_project_by_id(project['_id']) + task_types = project.get('task_types', {}) + + new_data = task_types.pop(name) + new_data.update(data) + + # Update project with new task_types + task_types[new_data['name']] = new_data + updated = self.update_project(project, {'task_types': task_types}) + + return updated['task_types'][new_data['name']] + + def delete_task_type(self, project, name): + + # Retrieve existing project task_types + project = self.get_project_by_id(project['_id']) + task_types = project.get('task_types', {}) + + self.update_project(project, {'task_types': task_types}) + + def get_path_to(self, entity): + '''Get a file system path to the provided entity. + + Arguments: + entity (dict): Data dict including (_type, _id, name, etc.) + + Returns: + pathlib.Path + ''' + + return self.fsfs.get_path_to(entity) diff --git a/construct/io/fsfs.py b/construct/io/fsfs.py new file mode 100644 index 0000000..7a47dad --- /dev/null +++ b/construct/io/fsfs.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +'''Lite reimplementation of github.com/danbradham/fsfs. + +Uses Path objects instead of passing strings and fsfs.Entry objects around. +''' + +from __future__ import absolute_import + +# Standard library imports +import logging +import shutil + +# Third party imports +from bson.objectid import ObjectId + +# Local imports +from ..compat import Path, basestring +from ..utils import update_dict, yaml_dump, yaml_load + + +_log = logging.getLogger(__name__) +search_pattern = '*/.data/uuid_*' +data_dir = '.data' +data_file = 'data' + + +def search_by_id(path, _id, max_depth=10): + '''Search by id''' + + for entry in search(path, max_depth): + if get_id(entry) == _id: + return entry + + +def search_by_tag(path, tags, max_depth=10): + '''Search by tag''' + + if isinstance(tags, basestring): + tags = [tags] + + for entry in search(path, max_depth): + if tags.issubset(set(get_tags(entry))): + yield entry + + +def search_by_name(path, name, max_depth=10): + '''Search by name''' + + potential_entries = [] + + for entry in search(path, max_depth): + if entry.name == name: + return entry + if entry.name in name: + potential_entries.append(entry) + + if potential_entries: + # In almost all cases the best match will be the shortest path + # with the least parts. + best = min( + potential_entries, + key=lambda x: (len(x.parts), len(str(x))) + ) + return best + + +def search(path, max_depth=10): + '''Yields directories with metadata.''' + + roots = list(path.iterdir()) + level = 0 + + while roots and level < max_depth: + + next_roots = [] + + for root in roots: + + if root.is_file() or root.name == '.data': + continue + + if exists(root): + yield root + + next_roots.extend(list(path.iterdir())) + + level += 1 + roots = next_roots + + +def exists(path): + '''Check if a path is already initialized.''' + + return ( + (path / data_dir).exists() and + (path / data_dir / data_file).exists() + ) + + +def init(path, _id=None): + '''Initialize .data for a directory. + + Arguments: + path (Path): Directory to initialize + _id (str): UUID value defaults to a new Bson ObjectID + ''' + + if not _id: + _id = str(ObjectId()) + + path_data_dir = path / data_dir + path_data_file = path_data_dir / data_file + path_uuid_file = path_data_dir / ('uuid_' + _id) + + path_data_dir.mkdir(parents=True, exist_ok=True) + path_data_file.touch(exist_ok=True) + + existing = list((path / data_dir).glob('uuid_*')) + if not existing: + path_uuid_file.touch(exist_ok=True) + + +def read(path, *keys): + '''Read metadata from directory + + Arguments: + path (Path): Directory containing metadata + *keys (List[str]): list of keys to retrieve. If no keys are passed + return all key, value pairs. + + Returns: + dict: key, value pairs stored at path + ''' + + path = Path(path) + + file = path / data_dir / data_file + + if not file.exists(): + raise OSError('Data file does not exist: ' + file.as_posix()) + + raw_data = file.read_text(encoding='utf-8') + if not raw_data: + return {} + + data = yaml_load(raw_data) + + if not keys: + return data + + if len(keys) == 1: + return data[keys[0]] + + return dict((k, data[k]) for k in keys) + + +def write(path, replace=False, **data): + '''Write metadata to directory + + Arguments: + path (str): Directory to write to + replace (bool): Overwrite directory data. Defaults to False. + **data (dict): key, value pairs to write + + Returns: + None + ''' + + path = Path(path) + + init(path) + + file = path / data_dir / data_file + + if not replace: + new_data = read(path) + update_dict(new_data, data) + else: + new_data = data + + file.write_bytes(yaml_dump(new_data)) + + +def set_id(path, value): + '''Set the id for the directory. + + Creates an empty file named uuid_ in the paths .data directory. + + Arguments: + path (Path): Directory to set id for + value (str): ID as string + + Returns: + None + ''' + + path = Path(path) + + existing = (path / data_dir).glob('uuid_*') + for file in existing: + file.unlink() + + path_uuid_file = path / data_dir / ('uuid_' + value) + path_uuid_file.touch(exist_ok=True) + + +def get_id(path): + '''Get the id of a directory. + + Returns: + str - uuid + ''' + + path = Path(path) + + existing = (path / data_dir).glob('uuid_*') + for file in existing: + return file.name.split('uuid_')[-1] + + +def delete(path, remove_root=False): + '''Delete a directory's metadata and tags. If remove_root is True, remove + the directory itself. + + Arguments: + path (Path): Directory to remove metadata and tags from + remove_root (bool): Delete the directory as well. Defaults to False. + + Returns: + None + ''' + + path = Path(path) + + if not path.exists(): + return + + path_data_dir = path / data_dir + if path_data_dir.exists(): + path_data_dir.unlink() + + if remove_root: + shutil.rmtree(path.as_posix()) + + +def get_tags(path): + '''Get a directory's tags. + + Returns: + List of tags as strings + ''' + + path = Path(path) + + return [ + f.name.split('tag_')[-1] + for f in (path / data_dir).glob('tag_*') + ] + + +def tag(path, *tags): + '''Tag a directory as an Entry with the provided tags. + + Arguments: + path (Path): Directory to tag + *tags (List[str]): Tags to add to directory like: 'asset' or 'project' + ''' + + assert len(tags), 'Must provide at least one tag.' + + path = Path(path) + + init(path) + + for tag in tags: + tag_file = path / data_dir / ('tag_' + tag) + tag_file.touch(exist_ok=True) + + +def untag(path, *tags): + '''Remove a tag from a directory. + + Arguments: + path (Path): Directory to remove tags from + *tags (List[str]): Tags to remove like: 'asset' or 'project' + ''' + + assert len(tags), 'Must provide at least one tag.' + + path = Path(path) + + if not path.exists(): + return + + for tag in tags: + tag_file = path / data_dir / ('tag_' + tag) + if tag_file.exists(): + tag_file.unlink() diff --git a/construct/io/fsfslayer.py b/construct/io/fsfslayer.py new file mode 100644 index 0000000..979d04b --- /dev/null +++ b/construct/io/fsfslayer.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + +# Local imports +from ..compat import Path +from . import fsfs +from .layer import IOLayer + + +_log = logging.getLogger(__name__) + + +class FsfsLayer(IOLayer): + + def __init__(self, api): + self.api = api + self.settings = api.settings + + def _get_projects(self, location, mount=None): + if mount: + root = self.api.get_mount(location, mount) + for entry in fsfs.search(root, max_depth=1): + yield entry + return + + for mount in self.settings['locations'][location].keys(): + root = self.api.get_mount(location, mount) + for entry in fsfs.search(root, max_depth=1): + yield entry + + def get_projects(self, location, mount=None): + for entry in self._get_projects(location, mount): + yield fsfs.read(entry) + + def get_project_by_id(self, _id, location=None, mount=None): + + location = self.api.context.location + + for project in self._get_projects(location, mount): + if fsfs.get_id(project) == _id: + return fsfs.read(project) + + def get_project(self, name, location, mount=None): + if mount: + root = self.api.get_mount(location, mount) + for entry in fsfs.search(root, max_depth=1): + if entry.name == name: + return fsfs.read(entry) + return + + for mount in self.settings['locations'][location].keys(): + root = self.api.get_mount(location, mount) + for entry in fsfs.search(root, max_depth=1): + if entry.name == name: + return fsfs.read(entry) + + def new_project(self, name, location, mount, data): + path = self.api.get_mount(location, mount) / name + + if fsfs.exists(path): + raise OSError('Project already exists.') + + fsfs.init(path, data['_id']) + fsfs.tag(path, 'project') + fsfs.write(path, **data) + + return fsfs.read(path) + + def update_project(self, project, data): + + path = self.get_path_to(project) + + if not path.exists(): + raise OSError('Project does not exist.') + + fsfs.write(path, replace=True, **data) + fsfs.set_id(path, data['_id']) + return fsfs.read(path) + + def delete_project(self, project): + + path = self.get_path_to(project) + fsfs.delete(path) + + def get_assets(self, project, bin=None, asset_type=None, group=None): + my_location = self.api.context.location + project = self.get_project_by_id(project['_id'], my_location) + assets = project['assets'] + + for asset in assets.values(): + asset['project_id'] = project['_id'] + asset_path = self.get_path_to(asset) + asset = fsfs.read(asset_path) + if bin and asset['bin'] != bin: + continue + if asset_type and asset['asset_type'] != asset_type: + continue + if group and asset['group'] != group: + continue + yield asset + + def get_asset(self, project, name): + potential_names = [] + for asset in self.get_assets(project): + if name in asset['name']: + potential_names.append(name) + if asset['name'] == name: + return asset + + if potential_names: + return min(potential_names, key=len) + + def new_asset(self, project, data): + path = self.get_path_to(data) + + fsfs.init(path, data['_id']) + fsfs.tag(path, 'asset') + fsfs.write(path, **data) + + return fsfs.read(path) + + def update_asset(self, asset, data): + path = self.get_path_to(asset) + + fsfs.write(path, replace=True, **data) + return fsfs.read(path) + + def delete_asset(self, asset): + path = self.get_path_to(asset) + fsfs.delete(path) + + def get_workfiles(self, asset): + return NotImplemented + + def get_publishes(self, asset): + return NotImplemented + + def get_latest_workfile(self, asset, name, task, file_type): + return NotImplemented + + def get_latest_publish(self, asset, name, task, file_type, instance=0): + return NotImplemented + + def get_next_workfile(self, asset, name, task, file_type): + return NotImplemented + + def get_next_publish(self, asset, name, task, file_type, instance=0): + return NotImplemented + + def new_workfile(self, asset, name, identifier, task, file_type, data): + return NotImplemented + + def new_publish(self, asset, name, identifier, task, file_type, data): + return NotImplemented + + def get_path_to(self, entity): + '''Get a file system path to the provided entity.''' + + my_location = self.api.context.location + + if entity['_type'] == 'project': + + entity_locations = entity.get('locations', {}) + + if not entity_locations: + for entry in self._get_projects(my_location): + if fsfs.get_id(entry) == entity['_id']: + return entry + else: + if my_location in entity_locations: + location = my_location + mount = entity['locations'][location] + else: + location, mount = entity['locations'].items()[0] + + root = self.api.get_mount(location, mount) + return root / entity['name'] + + if entity['_type'] == 'asset': + + for project in self._get_projects(my_location): + if fsfs.get_id(project) == entity['project_id']: + project_path = project + break + + location, mount = self.api.get_mount_from_path(project_path) + mount = self.api.get_mount(location, mount) + + project = fsfs.read(project_path) + + asset_tmpl = project['tree']['asset'] + asset_path = Path(asset_tmpl.format( + mount=mount, + project=project['name'], + bin=entity['bin'], + asset_type=entity['asset_type'], + group=entity['group'], + asset=entity['name'], + )) + return asset_path + + raise OSError('Could not find ' + entity['name']) diff --git a/construct/io/layer.py b/construct/io/layer.py new file mode 100644 index 0000000..e3c5dbd --- /dev/null +++ b/construct/io/layer.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +class IOLayer(object): + + def get_projects(self, location, mount=None): + return NotImplemented + + def get_project_by_id(self, _id, location=None, mount=None): + return NotImplemented + + def get_project(self, name, location, mount=None): + return NotImplemented + + def new_project(self, name, location, mount, data): + return NotImplemented + + def update_project(self, project, data): + return NotImplemented + + def delete_project(self, project): + return NotImplemented + + def get_assets(self, project, bin=None, asset_type=None, group=None): + return NotImplemented + + def get_asset(self, project, name): + return NotImplemented + + def new_asset(self, project, data): + return NotImplemented + + def update_asset(self, asset, data): + return NotImplemented + + def delete_asset(self, asset): + return NotImplemented + + def get_workfiles(self, asset): + return NotImplemented + + def get_publishes(self, asset): + return NotImplemented + + def get_latest_workfile(self, asset, name, task, file_type): + return NotImplemented + + def get_latest_publish(self, asset, name, task, file_type, instance=0): + return NotImplemented + + def get_next_workfile(self, asset, name, task, file_type): + return NotImplemented + + def get_next_publish(self, asset, name, task, file_type, instance=0): + return NotImplemented + + def new_workfile(self, asset, name, identifier, task, file_type, data): + return NotImplemented + + def new_publish(self, asset, name, identifier, task, file_type, data): + return NotImplemented + + def get_path_to(self, entity): + return NotImplemented diff --git a/construct/migrations/__init__.py b/construct/migrations/__init__.py new file mode 100644 index 0000000..4083deb --- /dev/null +++ b/construct/migrations/__init__.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging +from copy import deepcopy +from inspect import getmembers + +# Local imports +from ..compat import Path, basestring + + +_log = logging.getLogger(__name__) + + +class Migration(object): + + schema_type = '' + schema_version = (0, 1, 0) + + def __init__(self, api, entity): + self.api = api + self.entity = entity + + def log(self, direction): + name = self.__class__.__name__ + if _log.isEnabledFor(logging.DEBUG): + if self.__doc__: + _log.debug('%s: %s', name, self.__doc__) + _log.debug( + '%s.%s() from %s to %s', + name, + direction, + format_version(self.entity['schema_version']), + format_version(self.schema_version), + ) + + def log_failure(self): + _log.exception( + '%s: Migration Failed. All changes discarded.', + self.__class__.__name__, + ) + + def log_success(self, result): + if _log.isEnabledFor(logging.DEBUG): + _log.debug(result) + _log.debug(self.__class__.__name__ + ': okay!') + + def _forward(self): + self.log('forward') + try: + entity = self.forward() + except: + self.log_failure() + raise + + self.log_success(entity) + return entity + + def forward(self): + return self.entity + + def _backward(self): + self.log('backward') + try: + entity = self.backward() + except: + self.log_failure() + raise + + self.log_success(entity) + return entity + + def backward(self): + return self.entity + + +def split_version(version): + if version is None: + return + elif isinstance(version, tuple): + return version + elif isinstance(version, basestring): + return tuple([int(v) for v in version.split('.')]) + else: + raise ValueError('version: expected string or tuple got %s', version) + + +def format_version(version): + if version is None: + return + elif isinstance(version, basestring): + return version + elif isinstance(version, tuple): + return '.'.join([str(v) for v in version]) + else: + raise ValueError('version: expected string or tuple got %s', version) + + +def is_migration(obj): + if obj is Migration: + return False + try: + return issubclass(obj, Migration) + except TypeError: + return False + + +def initial_migration(api, project): + from .initial import InitialMigration + InitialMigration(api, project).forward() + + +def get_migrations(migrations_dir=None): + import sys + relative_dir = Path(__file__).parent + migrations_dir = migrations_dir or Path(__file__).parent + + if migrations_dir == relative_dir: + for f in migrations_dir.glob('*.py'): + if f.stem == '__init__': + continue + mod = __import__(f.stem, globals=globals(), level=1) + for _, member in getmembers(mod, is_migration): + yield member + else: + # Get migrations from another directory for testing purposes + old_path = sys.path[:] + sys.path.insert(0, str(migrations_dir)) + try: + for f in migrations_dir.glob('*.py'): + mod = __import__(f.stem) + for _, member in getmembers(mod, is_migration): + yield member + finally: + sys.path[:] = old_path + + +def applicable_migrations(migrations, schema_type, from_version, to_version): + + if not migrations: + return [] + + applicable = [m for m in migrations if schema_type == m.schema_type] + to_version = to_version or max([m.schema_version for m in applicable]) + + backward = False + if from_version > to_version: + backward = True + applicable = [ + m for m in applicable + if from_version > m.schema_version + and m.schema_version >= to_version + ] + + else: + applicable = [ + m for m in applicable + if from_version < m.schema_version + and m.schema_version <= to_version + ] + + applicable = sorted( + applicable, + key=lambda x: x.schema_version, + reverse=backward, + ) + + return applicable, from_version, to_version + + +def forward(api, entity, to_version=None, migrations_dir=None): + + name = entity.get('name', entity['_type']) + schema_type = entity['_type'] + schema_version = split_version(entity['schema_version']) + to_version = split_version(to_version) + + if to_version and schema_version > to_version: + _log.info( + 'Skipping migration: %s is newer than %s', + name, + format_version(to_version) + ) + return + + # Get applicable migrations + migrations, from_version, to_version = applicable_migrations( + get_migrations(migrations_dir), + schema_type, + schema_version, + to_version, + ) + + _log.info( + 'Migrating from %s to %s', + format_version(from_version), + format_version(to_version) + ) + _log.debug(entity) + try: + modified_entity = deepcopy(entity) + for m in migrations: + modified_entity = m(api, modified_entity)._forward() + except: + # TODO: rollback changes + pass + else: + # TODO: commit changes to data layers + pass + + return modified_entity + + +def backward(api, entity, to_version, migrations_dir=None): + + name = entity.get('name', entity['_type']) + schema_type = entity['_type'] + schema_version = entity['schema_version'] + schema_version = split_version(entity['schema_version']) + to_version = split_version(to_version) + + if schema_version < to_version: + _log.info( + 'Skipping migration: %s is older than %s', + name, + format_version(to_version) + ) + return + + # Get applicable migrations + _log.debug('XXXX %s -> %s', schema_version, to_version) + migrations, from_version, to_version = applicable_migrations( + get_migrations(migrations_dir), + schema_type, + schema_version, + to_version, + ) + + _log.info( + 'Migrating from %s to %s', + format_version(from_version), + format_version(to_version) + ) + _log.debug(entity) + try: + modified_entity = deepcopy(entity) + for m in migrations: + modified_entity = m(api, modified_entity)._forward() + except: + # TODO: rollback changes + pass + else: + # TODO: commit changes to data layers + pass + + return modified_entity diff --git a/construct/migrations/initial.py b/construct/migrations/initial.py new file mode 100644 index 0000000..3b5a421 --- /dev/null +++ b/construct/migrations/initial.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging + +# Third party imports +import fsfs + +# Local imports +from ..compat import Path +from . import Migration + + +_log = logging.getLogger(__name__) + + +class InitialMigration(object): + '''Migrates Construct 0.1.x projects. + + Special case - initial migration that needs to be updated until all old + projects are migrated over. + ''' + + project_tags = set(['project']) + folder_tags = set(['collection', 'asset_type', 'sequence']) + asset_tags = set(['asset', 'shot']) + task_tags = set(['task']) + workspace_tags = set(['workspace']) + + def __init__(self, api, path): + self.api = api + self.path = path + + def forward(self): + query = fsfs.search( + root=str(self.path), + depth=3, + levels=10, + skip_root=False + ) + for entry in query: + self.migrate(entry) + + def backward(self): + pass + + def migrate(self, entry): + tags = set(entry.tags) + + if tags & self.project_tags: + self.migrate_project(entry) + elif tags & self.folder_tags: + self.migrate_folder(entry) + elif tags & self.asset_tags: + self.migrate_asset(entry) + elif tags & self.task_tags: + self.migrate_task(entry) + elif tags & self.workspace_tags: + self.migrate_workspace(entry) + else: + print('WARNING: could not migrate %s' % entry) + + def migrate_project(self, project): + _log.debug('Found project: %s' % project) + + data = project.read() + + data.update( + name=project.name, + locations={}, + ) + + location_mount = self.api.get_mount_from_path(project.path) + if location_mount: + location, mount = location_mount + data['locations'][location] = mount + + # Fill data with defaults from schema + data = self.api.schemas.validate('project', data) + + # Update tree + first_child = project.children().one() + if first_child: + folders_path = Path(first_child.path).parent + try: + folders = str(folders_path.relative_to(project.path)) + except ValueError: + folders = '' + data['tree']['folders'] = folders + + project.tag('project') + project.uuid = data['_id'] + project.write(**data) + + def migrate_folder(self, folder): + project = folder.parents().tags('project').one() + parent = folder.parent() + _log.debug('Found folder: %s/%s' % (parent.name, folder.name)) + + data = folder.read() + data.update( + name=folder.name, + project_id=project.uuid, + parent_id=parent.uuid, + ) + data = self.api.schemas.validate('folder', data) + + folder.tag('folder') + folder.uuid = data['_id'] + folder.write(**data) + + def migrate_asset(self, asset): + project = asset.parents().tags('project').one() + parent = asset.parent() + _log.debug('Found asset: %s/%s' % (parent.name, asset.name)) + + if 'shot' in asset.tags: + asset_type = 'shot' + else: + asset_type = 'asset' + + data = asset.read() + data.update( + name=asset.name, + project_id=project.uuid, + parent_id=parent.uuid, + asset_type=asset_type, + ) + data = self.api.schemas.validate('asset', data) + + asset.tag('asset') + asset.uuid = data['_id'] + asset.write(**data) + + def migrate_task(self, task): + project = task.parents().tags('project').one() + parent = task.parent() + _log.debug('Found task: %s/%s' % (parent.name, task.name)) + + data = task.read() + data.update( + name=task.name, + project_id=project.uuid, + parent_id=parent.uuid, + ) + data = self.api.schemas.validate('task', data) + + task.tag('task') + task.uuid = data['_id'] + task.write(**data) + + def migrate_workspace(self, workspace): + project = workspace.parents().tags('project').one() + parent = workspace.parent() + _log.debug('Found workspace: %s/%s' % (parent.name, workspace.name)) diff --git a/construct/models.py b/construct/models.py deleted file mode 100644 index b336175..0000000 --- a/construct/models.py +++ /dev/null @@ -1,277 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import getpass -import datetime -import fsfs -from scandir import scandir -from construct.vendor.lucidity.error import ParseError -from construct.errors import ConfigurationError -from construct.utils import cached_property - - -factory = fsfs.EntryFactory() - - -class Entry(factory.Entry): - '''Base class for all Construct Entry types''' - - default_thumbnail = '' - - def get_comments(self): - '''Get Entry comments''' - - try: - return self.read('comments') - except KeyError: - return [] - - def new_comment(self, body): - '''Add a new comment to Entry''' - - comment = dict( - user=getpass.getuser(), - time=datetime.datetime.utcnow(), - body=body - ) - comments = self.get_comments() - comments.append(comment) - self.write(comments=comments) - - def get_thumbnail(self): - '''Get Entry thumbnail''' - - try: - return self.read_file('thumbnail') - except KeyError: - return fsfs.File(self.default_thumbnail, 'rb') - - def set_thumbnail(self, file): - '''Set Entry thumbnail to file''' - - self.write_file('thumbnail', file) - - def get_status(self): - '''Get Entry status''' - - try: - return self.read('status') - except KeyError: - self.set_status('waiting') - return 'waiting' - - def set_status(self, status): - '''Set status of Entry''' - - from construct import config - statuses = config['STATUSES'].keys() - - if status not in statuses: - raise ValueError('Status must be one of: ' + str(statuses)) - - self.write(status=status) - - -factory.Entry = Entry - - -class Project(Entry): - - @property - def collections(self): - return self.children(levels=1).tags('collection') - - @property - def asset_types(self): - return self.children(levels=2).tags('asset_type') - - @property - def assets(self): - return self.children(levels=3).tags('asset') - - @property - def sequences(self): - return self.children(levels=2).tags('sequence') - - @property - def shots(self): - return self.children(levels=3).tags('shot') - - -class Collection(Entry): - - @property - def sequences(self): - return self.children(levels=1).tags('sequence') - - @property - def asset_types(self): - return self.children(levels=1).tags('asset_type') - - @property - def assets(self): - return self.children(levels=2).tags('asset') - - @property - def shots(self): - return self.children(levels=2).tags('shot') - - -class Sequence(Entry): - - @property - def shots(self): - return self.children(levels=1).tags('shot') - - -class Shot(Entry): - - @property - def tasks(self, *tags): - return self.children(levels=1).tags('task', *tags) - - -class AssetType(Entry): - - type_for_tag = 'asset_type' - - @property - def assets(self): - return self.children(levels=1).tags('asset') - - -class Asset(Entry): - - @property - def tasks(self, *tags): - return self.children(levels=1).tags('task', *tags) - - @cached_property - def type(self): - return self.parent().name - - -class Task(Entry): - - @cached_property - def config(self): - from construct import config - tasks_cfg = config['TASK_TYPES'] - if self.name in tasks_cfg: - return tasks_cfg[self.name] - else: - for tag in self.tags: - if tag in tasks_cfg: - return tasks_cfg[tag] - - @property - def short(self): - return self.config['short'] - - @property - def color(self): - return self.config['color'] - - @property - def icon(self): - return self.config['icon'] - - @property - def workspaces(self, *tags): - return self.children(levels=1).tags('workspace', *tags) - - @property - def publishes(self, *tags): - return self.children(levels=1).tags('publish', *tags) - - def get_latest_publish(self): - publishes = list(self.publishes) - try: - return publishes[-1] - except IndexError: - pass - - -class Workspace(Entry): - - @cached_property - def config(self): - from construct import config - - software_cfg = config['SOFTWARE'] - for app_name, app_cfg in software_cfg.items(): - if app_cfg['host'] == self.name: - return app_cfg - else: - for tag in self.tags: - if app_cfg['host'] == tag: - return app_cfg - - @property - def icon(self): - return self.config['icon'] - - def get_work_files(self): - import construct - path_template = construct.get_path_template('workspace_file') - versions = {} - for f in scandir(self.path): - try: - data = path_template.parse(f.name) - data['version'] = int(data['version']) - except ParseError: - continue - versions[f.name] = data - return versions - - def get_next_version(self, name, ext): - task = self.parent().short - versions = self.get_work_files() - version = 0 - - for data in versions.values(): - is_match = ( - data['name'] == name and - data['task'] == task and - data['ext'] == ext - ) - if is_match: - version = max(version, data['version']) - - return version + 1 - - def new_version(self, user, name, version, file_type, file): - import os - relative_path = os.path.relpath(file, self.path) - version = Version( - user=user, - file_type=file_type, - file=relative_path, - name=name, - version=version, - ) - data = self.read() - data.setdefault('versions', []) - data['versions'].append(version) - self.write(**data) - - -class EmbeddedModel(dict): - - __fields__ = [] - - def __init__(self, *args, **kwargs): - super(Version, self).__init__(*args, **kwargs) - self.__dict__ = self - for field in self.__fields__: - if field not in self: - raise ValueError('Missing keyword argument: %s' % field) - - -class Version(EmbeddedModel): - - __field__ = ['user', 'name', 'version', 'file_type', 'file'] - - -def is_entry(obj): - '''Returns True if obj is an instance of Entry or EntryProxy''' - return isinstance(obj, (factory.Entry, factory.EntryProxy)) diff --git a/construct/path.py b/construct/path.py new file mode 100644 index 0000000..6a8614e --- /dev/null +++ b/construct/path.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import os + +# Local imports +from .constants import DEFAULT_PATHS +from .utils import unipath + + +class Path(list): + '''A list of folders used to lookup resources like settings, icons, and + extensions. + ''' + + def __init__(self, path=None): + super(Path, self).__init__() + self._custom = bool(path) + self._custom_path = path + + def load(self): + if self._custom: + self.extend( + unipath(p) for p in self._custom_path + ) + return + try: + env_paths = os.environ['CONSTRUCT_PATH'].strip(os.pathsep) + self.extend( + unipath(p) for p in env_paths.split(os.pathsep) + ) + except KeyError: + pass + self.extend(DEFAULT_PATHS) + + def unload(self): + self[:] = [] + + def find(self, resource): + '''Find a resource in one of this objects paths. + + Examples: + >>> path = Path(['~/.construct']) + >>> path.find('construct.yaml') + '~/.construct/construct.yaml' + ''' + for path in self: + potential_path = path / resource + if potential_path.exists(): + return potential_path diff --git a/construct/schemas/__init__.py b/construct/schemas/__init__.py new file mode 100644 index 0000000..fc9634a --- /dev/null +++ b/construct/schemas/__init__.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import datetime +import getpass +import json +import os + +# Third party imports +from bson.objectid import ObjectId +from cachetools import cached +from cerberus import Validator +from cerberus.schema import SchemaRegistry + +# Local imports +from ..compat import Path +from ..errors import ValidationError +from ..utils import yaml_dump, yaml_load + + +package_path = Path(__file__).parent.resolve() + + +class SchemaNotFound(Exception): pass +class SchemaError(Exception): pass + + +class ConstructValidator(Validator): + + def _validate_type_objectid(self, value): + return ObjectId.is_valid(value) + + def _normalize_default_setter_objectid(self, document): + return str(ObjectId()) + + def _normalize_default_setter_utcnow(self, document): + return datetime.datetime.utcnow() + + def _normalize_default_setter_getuser(self, document): + return getpass.getuser() + + def _normalize_default_setter_empty_string(self, document): + return '' + + def _normalize_default_setter_empty_list(self, document): + return [] + + def _normalize_default_setter_empty_dict(self, document): + return {} + + @property + def errors_yaml(self): + return yaml_dump(self.errors) + + @property + def errors_json(self): + return json.dumps(self.errors) + + +@cached(cache={}) +def get_validator(schema_name, **kwargs): + '''Get a validator for one of Constructs builtin schemas. + + Examples: + new_project = {...} + v = get_validator('project') + if v.validate(new_project): + new_project = v.normalized(new_project) + ''' + return new_validator(get_schema(schema_name), **kwargs) + + +def new_validator(schema, **kwargs): + kwargs.setdefault('schema_registry', _schema_registry) + return ConstructValidator(schema, **kwargs) + + +@cached(cache={}) +def get_schema(name): + '''Get a Cerberus schema dict by name.''' + + schema_file = package_path / (name + '.yaml') + if not schema_file.is_file(): + raise SchemaNotFound('Could not find a schema named %s' % name) + + return yaml_load(schema_file.read_text(encoding='utf-8')) + + +def validate(schema_name, data, **kwargs): + kwargs.setdefault('allow_unknown', True) + v = get_validator(schema_name, **kwargs) + data = v.validated(data) + if not data: + raise ValidationError( + 'Data does not match "%s" schema.\n%s' + % (schema_name, v.errors_yaml), + errors=v.errors + ) + return data + + +def ls(subdir=None): + '''Yields schemas from the schemas_root or a specified subdir. + + Returns: + generator yielding (schema_name, schema_dict) pairs + ''' + + if subdir: + root = package_path / subdir + else: + root = package_path + + for schema_file in root.glob('*.yaml'): + schema_name = schema_file.stem + if subdir: + schema_name = subdir + '/' + schema_name + yield schema_name, get_schema(schema_name) + + +_schema_registry = SchemaRegistry() +for schema_name, schema in ls(): + _schema_registry.add(schema_name, schema) diff --git a/construct/schemas/asset.yaml b/construct/schemas/asset.yaml new file mode 100644 index 0000000..5f5eaf4 --- /dev/null +++ b/construct/schemas/asset.yaml @@ -0,0 +1,50 @@ +schema_version: + type: string + default: 0.1.0 + +_id: + type: objectid + default_setter: objectid + +_type: + type: string + default: asset + +project_id: + type: objectid + required: True + +created_at: + type: datetime + default_setter: utcnow + +created_by: + type: string + default_setter: getuser + +name: + type: string + required: True + +bin: + type: string + required: True + +asset_type: + type: string + required: True + +group: + type: string + default_setter: empty_string + +tags: + type: list + schema: + type: string + +comments: + type: list + schema: + type: dict + schema: comment diff --git a/construct/schemas/comment.yaml b/construct/schemas/comment.yaml new file mode 100644 index 0000000..2da81af --- /dev/null +++ b/construct/schemas/comment.yaml @@ -0,0 +1,15 @@ +schema_version: + type: string + default: 0.1.0 + +created_at: + type: datetime + default_setter: utcnow + +created_by: + type: string + default_setter: getuser + +text: + type: string + required: True diff --git a/construct/schemas/project.yaml b/construct/schemas/project.yaml new file mode 100644 index 0000000..89f81f1 --- /dev/null +++ b/construct/schemas/project.yaml @@ -0,0 +1,200 @@ +schema_version: + type: string + default: 0.1.0 + +_id: + type: objectid + default_setter: objectid + +_type: + type: string + default: project + +name: + type: string + required: True + +description: + type: string + default_setter: empty_string + +created_at: + type: datetime + default_setter: utcnow + +created_by: + type: string + default_setter: getuser + +fps: + type: float + default: 24.0 + +people: + type: dict + allow_unknown: True + valuesrules: + type: string + required: False + +tree: + type: dict + allow_unknown: True + valuesrules: + type: string + schema: + bin: + type: string + asset: + type: string + workspace: + type: string + publish: + type: string + review: + type: string + render: + type: string + file: + type: string + file_sequence: + type: string + default: + asset: '{mount}/{project}/{bin}/{asset}' + workspace: '{mount}/{project}/{bin}/{asset}/work/{task}/{host}' + publish: '{mount}/{project}/{bin}/{asset}/publish/{task_short}_{name}/v{version:0>3d}' + review: '{mount}/{project}/{bin}/{asset}/review/{task}/{host}' + render: '{mount}/{project}/{bin}/{asset}/render/{task}/{host}' + file: '{task_short}_{name}_v{version:0>3d}.{ext}' + file_sequence: '{task_short}_{name}_v{version:0>3d}.{frame}.{ext}' + +asset_types: + type: dict + allow_unknown: True + valuesrules: + type: dict + require_all: True + schema: + name: + type: string + icon: + type: string + default: empty_string + order: + type: integer + default: + asset: + name: asset + icon: asset.svg + order: 0 + shot: + name: shot + icon: shot.svg + order: 1 + +task_types: + type: dict + allow_unknown: True + valuesrules: + type: dict + require_all: True + schema: + name: + type: string + short: + type: string + icon: + type: string + default: empty_string + order: + type: integer + default: + design: + name: design + short: dsn + icon: design.svg + order: 0 + model: + name: model + short: mdl + icon: model.svg + order: 1 + texture: + name: texture + short: tx + icon: texture.svg + order: 2 + shade: + name: shade + short: shd + icon: shade.svg + order: 3 + rig: + name: rig + short: rig + icon: rig.svg + order: 4 + animate: + name: animate + short: anim + icon: anim.svg + order: 5 + fx: + name: fx + short: fx + icon: fx.svg + order: 6 + light: + name: light + short: lgt + icon: light.svg + order: 7 + composite: + name: composite + short: comp + icon: comp.svg + order: 8 + +bins: + type: dict + allow_unknown: True + valuesrules: + type: dict + require_all: True + schema: + name: + type: string + icon: + type: string + default_setter: empty_string + order: + type: integer + default: + assets: + name: assets + icon: asset.svg + order: 0 + shots: + name: shots + icon: shot.svg + order: 1 + +assets: + type: dict + allow_unknown: True + default_setter: empty_dict + valuesrules: + type: dict + require_all: True + schema: + _id: + type: objectid + name: + type: string + bin: + type: string + asset_type: + type: string + group: + type: string + default_setter: empty_string diff --git a/construct/schemas/settings.yaml b/construct/schemas/settings.yaml new file mode 100644 index 0000000..3b26a66 --- /dev/null +++ b/construct/schemas/settings.yaml @@ -0,0 +1,29 @@ +schema_version: + type: string + default: 0.1.0 + +my_location: + type: string + required: True + +my_mount: + type: string + required: True + +locations: + type: dict + valueschema: + type: dict + required: True + +mongodb_uri: + type: string + default_setter: empty_string + +extensions: + type: list + default_setter: empty_list + +logging: + type: dict + default_setter: empty_dict diff --git a/construct/schemas/software.yaml b/construct/schemas/software.yaml new file mode 100644 index 0000000..8fbfcdc --- /dev/null +++ b/construct/schemas/software.yaml @@ -0,0 +1,44 @@ +schema_version: + type: string + default: 0.1.0 + +label: + type: string + required: True + +icon: + type: string + required: True + +host: + type: string + required: True + +cmd: + type: dict + required: True + schema: + linux: + anyof_type: [string, list] + mac: + anyof_type: [string, list] + win: + anyof_type: [string, list] + +args: + type: list + default_setter: empty_list + +env: + type: dict + valuesrules: + anyof_type: [string, list, dict] + default_setter: empty_dict + +template: + type: string + default_setter: empty_string + +extensions: + type: list + default_setter: empty_list diff --git a/construct/schemas/version.yaml b/construct/schemas/version.yaml new file mode 100644 index 0000000..856b39c --- /dev/null +++ b/construct/schemas/version.yaml @@ -0,0 +1,67 @@ +schema_version: + type: string + default: 0.1.0 + +_id: + type: objectid + default_setter: objectid + +_type: + type: string + default: version + +created_at: + type: datetime + default_setter: utcnow + +created_by: + type: string + default_setter: getuser + +name: + type: string + required: True + +instance: + type: integer + default: 0 + +version: + type: integer + required: True + +file_type: + type: string + required: True + +identifier: + type: string + required: True + +comments: + type: list + schema: + type: dict + schema: comment + +thumbnail: + type: binary + +images: + type: list + schema: + type: binary + +is_sequence: + type: boolean + default: False + +start_frame: + type: integer + +end_frame: + type: integer + +padding: + type: integer + diff --git a/construct/settings.py b/construct/settings.py new file mode 100644 index 0000000..2f17091 --- /dev/null +++ b/construct/settings.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import copy +import logging +import os +import shutil +import sys + +# Local imports +from . import schemas +from .compat import basestring, wraps +from .constants import ( + DEFAULT_SETTINGS, + SETTINGS_FILE, + USER_PATH, + USER_SETTINGS_FILE, +) +from .errors import InvalidSettings, ValidationError +from .utils import ensure_exists, unipath, yaml_dump, yaml_load + + +_log = logging.getLogger(__name__) + + +class Settings(dict): + '''Provides dict access to your settings file. + + Attributes: + path: :class:`construct.path.Path` used to locate settings + file: Path to construct.yaml file + folder: Path containing construct.yaml file + ''' + + structure = [ + 'extensions', + 'icons', + 'templates' + ] + + def __init__(self, path): + self.path = path + self.file = None + self.folder = None + self.is_loaded = False + self._sections = {} + + def add_section(self, name, schema=None): + section = Section(name, self.folder / name, schema) + section.load() + self._sections[name] = section + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + return self._sections[key] + + def load(self): + v = schemas.get_validator('settings') + if not v.validate(DEFAULT_SETTINGS): + raise InvalidSettings( + 'DEFAULT_SETTINGS have the following errors:\n' + + v.errors_yaml + ) + + self.update(**DEFAULT_SETTINGS) + self.file = self.path[-1] / SETTINGS_FILE + potential_settings_file = find_in_paths(self.path, SETTINGS_FILE) + + if potential_settings_file: + self.file = potential_settings_file + _log.debug('Loading settings from %s' % self.file) + + # Read settings from file + data = potential_settings_file.read_text(encoding='utf-8') + if data: + file_data = yaml_load(data) + else: + file_data = {} + + if not v.validate(file_data): + raise InvalidSettings( + str(self.file) + ' contains the following errors:\n' + + v.errors_yaml + ) + + self.update(**file_data) + else: + _log.debug( + 'Settings file not found.' + ' Writing default settings to ' + str(self.file) + ) + restore_default_settings(self.path[-1]) + + self.folder = self.file.parent + + # Make sure our settings folders exist + ensure_exists(*[self.folder / f for f in self.structure]) + + self.is_loaded = True + + def unload(self): + self.clear() + self.file = None + self.folder = None + self.is_loaded = False + + def save(self): + '''Save these settings.''' + + if self.is_loaded: + data = self.yaml(exclude=['software']) + + # Write modified settings + self.file.write_bytes(data) + + def yaml(self, exclude=None): + '''Serialize these settings as yaml.''' + + settings_to_encode = copy.deepcopy(dict(self)) + exclude = (exclude or []) + for key in exclude: + settings_to_encode.pop(key, None) + return yaml_dump(settings_to_encode) + + +def load_on_modified(method): + '''Section method wrapper. Load when mtime changes.''' + @wraps(method) + def call_method(self, *args, **kwargs): + mtime = self.folder.stat().st_mtime + if self.mtime != mtime: + self.load() + return method(self, *args, **kwargs) + return call_method + + +class Section(dict): + + def __init__(self, name, folder, schema=None): + self.name = name + self.folder = folder + self.schema = schema + self.mtime = None + self.files = {} + self.load() + + def _get_file(self, name): + return self.folder / (name + '.yaml') + + def _get_validator(self): + if not self.schema: + return + + if isinstance(self.schema, basestring): + return schemas.get_validator(self.schema) + + if isinstance(self.schema, dict): + return schemas.new_validator(self.schema) + + raise schemas.SchemaError('Could not load schema: %s' % self.schema) + + def validate(self, data, **kwargs): + v = self._get_validator() + if not v: + return data + + valid_data = v.validated(data, **kwargs) + if not valid_data: + raise ValidationError( + '%s Section: Data is invalid.\n%s' + % (self.name, v.errors_yaml), + errors=v.errors + ) + + return valid_data + + def load(self): + ensure_exists(self.folder) + self.mtime = self.folder.stat().st_mtime + + section_files = {} + section_data = {} + for file in self.folder.iterdir(): + + if file.suffix not in ('.yml', '.yaml'): + continue + + raw_data = file.read_text(encoding='utf-8') + data = self.validate(yaml_load(raw_data)) + + section_data[file.stem] = data + section_files[file.stem] = file + + self.clear() + self.update(section_data) + self.files.clear() + self.files.update(section_files) + + def write(self, name, data): + data = self.validate(data) + dict.__setitem__(self, name, data) + + file = self._get_file(name) + file.write_bytes(yaml_dump(data)) + return data + + def delete(self, name): + dict.pop(self, name, None) + file = self._get_file(name) + if file.is_file(): + file.unlink() + + # Setup standard dict methods + __getitem__ = load_on_modified(dict.__getitem__) + read = __getitem__ + __delitem__ = delete + pop = delete + __setitem__ = write + items = load_on_modified(dict.items) + keys = load_on_modified(dict.keys) + + +def restore_default_settings(where): + '''Create a default configuration in the provided folder.''' + + where = unipath(where) + if where.is_dir(): + shutil.rmtree(str(where)) + + ensure_exists(where) + ensure_exists(*[where / f for f in Settings.structure]) + + # Write default settings + settings_file = where / SETTINGS_FILE + settings_file.write_bytes(yaml_dump(DEFAULT_SETTINGS)) + + +def find_in_paths(paths, resource): + '''Find a resource in a list of paths''' + + for path in paths: + potential_path = path / resource + if potential_path.exists(): + return potential_path diff --git a/construct/signals.py b/construct/signals.py deleted file mode 100644 index 85269fe..0000000 --- a/construct/signals.py +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -__all__ = [ - 'route', - 'chain', - 'send', - 'connect', - 'disconnect', -] - -import logging -from collections import defaultdict -from fnmatch import fnmatch -from contextlib import contextmanager -from construct.utils import dummy_ctxmanager -from construct.types import weakset - - -ALL = '*' -_subscribers = defaultdict(weakset) -_suppress = None -_log = logging.getLogger(__name__) - - -def is_suppressed(identifier): - if _suppress is None: - return False - - if _suppress == ALL or any([fnmatch(identifier, k) for k in _suppress]): - _log.debug('suppressed %s' % identifier) - return True - - -def suppressed_when(condition, *identifiers): - if condition: - return suppressed(*identifiers) - return dummy_ctxmanager() - - -@contextmanager -def suppressed(*identifiers): - global _suppress - _suppress = list(identifiers) or ALL - _log.debug('suppressing %s' % _suppress) - try: - yield - finally: - _suppress = None - - -def get_subscribers(identifier): - '''Yield all subscribers for identifier, including fuzzy matches. - - Arguments: - identifier (str): Signal identifier - - Yields: - callable subscribers - ''' - - for subscriber in _subscribers[identifier]: - yield subscriber - - for key in _subscribers.keys(): - if key == identifier: - continue - - # Fuzzy Subscription - if fnmatch(identifier, key): - for subscriber in _subscribers[key]: - yield subscriber - - -def send(identifier, *args, **kwargs): - '''Send signal - - Arguments: - identifier (str): signal identifier - ''' - - if is_suppressed(identifier): - return - - results = [] - for subscriber in get_subscribers(identifier): - results.append(subscriber(*args, **kwargs)) - return results - - -def chain(identifier, *args, **kwargs): - '''Chain signal subscribers - - Arguments: - identifier (str): signal identifier - ''' - if is_suppressed(identifier): - return - - subscribers = list(get_subscribers(identifier)) - if not subscribers: - return - - result = subscribers[0](*args, **kwargs) - if len(subscribers) == 1: - return result - - for subscriber in subscribers[1:]: - result = subscriber(result) - - return result - - -def route(identifier): - '''Connect function to signal via decorator, only works with functions. - - Arguments: - identifier (str): signal identifier - ''' - - def connect_to_signal(obj): - connect(identifier, obj) - return obj - return connect_to_signal - - -def connect(identifier, obj): - '''Connect function to signal - - Arguments: - identifier (str): signal identifier - obj (callable): signal subscriber - ''' - - _subscribers[identifier].add(obj) - - -def disconnect(identifier, obj): - '''Disconnect function to signal - - Arguments: - identifier (str): signal identifier - obj (callable): signal subscriber - ''' - - _subscribers[identifier].discard(obj) diff --git a/construct/stats.py b/construct/stats.py deleted file mode 100644 index b34d040..0000000 --- a/construct/stats.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from functools import wraps -from construct.constants import TIMER -from construct.utils import get_qualname -from contextlib import contextmanager -import logging - - -_log = logging.getLogger(__name__) - - -class Stats(object): - '''Collection of :class:`Action` execution stats - - Attributes: - number_of_failures - number_of_successes - number_of_skips - total_execution_time - group_times - average_group_time - task_times - average_task_time - ''' - # TODO: Implement stats - - -@contextmanager -def log_time(msg): - - start_time = TIMER() - try: - yield - finally: - _log.info('%s: %.8fs', msg, TIMER() - start_time) - - -def log_call(fn): - '''Log execution time of a wrapped function''' - @wraps(fn) - def _logged_call(*args, **kwargs): - start_time = TIMER() - try: - return fn(*args, **kwargs) - finally: - _log.info('%s(): %.8fs', get_qualname(fn), TIMER() - start_time) - return _logged_call diff --git a/construct/tasks.py b/construct/tasks.py deleted file mode 100644 index 6166b23..0000000 --- a/construct/tasks.py +++ /dev/null @@ -1,1021 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -__all__ = [ - 'RequestThread', - 'AsyncRequest', - 'AsyncTask', - 'Request', - 'Task', - 'TaskCollection', - 'task', - 'async_task', - 'sort_tasks', - 'group_tasks', - 'requires', - 'skips', - 'pass_context', - 'pass_args', - 'pass_kwargs', - 'params', - 'available', - 'done', - 'failure', - 'success', - 'store', - 'artifact', - 'returns', - 'kwarg', -] - -import threading -import sys -import inspect -import logging -import six -from operator import attrgetter -from collections import OrderedDict -from construct.compat import basestring -from construct.constants import * -from construct.context import _ctx_stack, _req_stack -from construct.utils import ( - get_callable_name, - pop_attr, - ensure_callable, -) -from construct.types import Priority -from construct.errors import TimeoutError, TaskError -from construct import signals - - -_log = logging.getLogger(__name__) -DEFAULT_PRIORITY = Priority(0) - - -class RequestThread(threading.Thread): - - def __init__(self, request, retries=0, interval=1): - super(RequestThread, self).__init__() - self.retries = retries - self.interval = interval - self.request = request - self._shutdown = threading.Event() - self._stopped = threading.Event() - self._started = threading.Event() - self.request.set_status(WAITING) - - def start(self): - '''Pass context through to thread''' - self.request.set_status(PENDING) - super(RequestThread, self).start() - - def run(self): - - try: - - self._started.set() - retries = 0 - self.request.push() - - while True: - - try: - # Setup task - self.request.set_status(RUNNING) - self.request._before_task() - - # Run task - result = self.request.task( - *self.request.args, - **self.request.kwargs - ) - - # Finish task - self.request.set_value(result) - self.request._after_task() - break - - except: - self.request.set_exception(*sys.exc_info()) - if retries >= self.retries: - break - - retries += 1 - if self._shutdown.wait(self.interval): - break - - finally: - - self.request.pop() - self._stopped.set() - - def started(self): - return self._started.is_set() - - def stopped(self): - return self._stopped.is_set() - - def stop(self): - self._shutdown.set() - self._stopped.wait() - if self.isAlive(): - self.join() - - def ready(self): - return self.request._status in DONE_STATUSES - - def _get_ready(self, propagate=True): - - if self.request.success: - return self.request._value - - if self.request.failed: - if propagate: - six.reraise(*self.request._exc) - return self.request._exc - - def get(self, timeout=None, interval=0.2, propagate=True): - - st = TIMER() - while not self.ready(): - if timeout is not None and TIMER() - st > timeout: - raise TimeoutError('Result not ready yet...') - SLEEP(interval) - - return self._get_ready(propagate) - - -class Request(object): - - def __init__(self, task, ctx=None, args=None, kwargs=None): - self.task = task - self.ctx = ctx - self.args = args or () - self.kwargs = kwargs or {} - self.retries = 0 - self._enabled = True - self._status = None - self._exc = None - self._value = None - self.push() - self.set_status(WAITING) - self.pop() - - def push(self): - if _ctx_stack.top is not self.ctx: - _ctx_stack.push(self.ctx) - if _req_stack.top is not self: - _req_stack.push(self) - - def pop(self): - if _req_stack.top is self: - _req_stack.pop() - if _ctx_stack.top is self.ctx: - _ctx_stack.pop() - - @property - def value(self, value): - return self._value - - def set_value(self, value): - self._value = value - self.set_status(SUCCESS) - - @property - def exception(self): - return self._exc - - def set_exception(self, *exc_info): - self._exc = exc_info - self.set_status(FAILED) - - @property - def status(self): - - return self._status - - def set_status(self, status): - - if self._status == status: - return - - self.push() - try: - last_status = status - self._status = status - signals.send('request.status.changed', self, last_status, status) - finally: - self.pop() - - @property - def enabled(self): - return self._enabled - - def set_enabled(self, value): - self._enabled = value - signals.send('request.enabled', self, self._enabled) - - @property - def success(self): - return self._status == SUCCESS - - @property - def failed(self): - return self._status == FAILED - - @property - def done(self): - return self._status in DONE_STATUSES - - @property - def ready(self): - return self._status in DONE_STATUSES - - def _before_task(self): - if self.ctx is not None: - self.args, self.kwargs = self.task.get_params(self.ctx) - - def _after_task(self): - if self.ctx is not None: - self.task.run_callbacks(self.ctx, self._value) - - def get(self, propagate=True): - - if self.success: - return self._value - - if self.failed: - if propagate: - six.reraise(*self._exc) - return self._exc - - try: - - # Setup task - self.set_status(RUNNING) - self._before_task() - self.push() - - # Run task - result = self.task(*self.args, **self.kwargs) - - # Finish task - self.set_value(result) - self._after_task() - return result - - except: - - self.retries += 1 - exc_info = sys.exc_info() - self.set_exception(*exc_info) - if propagate: - six.reraise(*exc_info) - return exc_info - - finally: - - self.pop() - - -class AsyncRequest(Request): - - def __init__(self, task, ctx=None, args=None, kwargs=None): - super(AsyncRequest, self).__init__(task, ctx, args, kwargs) - self._thread = RequestThread(self) - - def get(self, timeout=None, interval=0.2, propagate=True): - - try: - - self.push() - - if not self._thread.started(): - self._thread.start() - - return self._thread.get(timeout, interval, propagate) - - finally: - - self.pop() - - -class Task(object): - - request_cls = Request - - def __init__(self, fn, identifier=None, description=None, - priority=DEFAULT_PRIORITY, requires=None, skips=None, - arg_getters=None, kwarg_getters=None, callbacks=None, - available=True, parent=None): - self.fn = fn - if not isinstance(priority, Priority): - priority = Priority(priority) - self.priority = priority - self.identifier = identifier - self.description = description - self.requires = requires or [] - self.skips = skips or [] - self.arg_getters = arg_getters or [] - self.kwarg_getters = kwarg_getters or {} - self.callbacks = callbacks or [] - self._available = available - self.parent = parent - - def __call__(self, *args, **kwargs): - if self.bound_method: - return self.fn(self.parent, *args, **kwargs) - else: - return self.fn(*args, **kwargs) - - def __repr__(self): - return '<{} {} at 0x{}>(identifier={!r})'.format( - ('unbound', 'bound')[self.bound], - self.__class__.__name__, - id(self), - self.identifier - ) - - def __get__(self, obj, type): - if obj is None: - return self - - bind_task = self.clone(parent=obj) - - for name, member in inspect.getmembers(type): - if member is self: - setattr(obj, name, bind_task) - - return bind_task - - @property - def bound(self): - return self.parent is not None - - @property - def bound_method(self): - spec = inspect.getargspec(self.fn) - return self.bound and 'self' in spec.args - - @property - def func_name(self): - return get_callable_name(self.fn) - - def clone(self, **kwargs): - '''Create a new Task instance, overriding any kwargs you need to''' - - kwargs.setdefault('fn', self.fn) - kwargs.setdefault('identifier', self.identifier) - kwargs.setdefault('description', self.description) - kwargs.setdefault('priority', self.priority) - kwargs.setdefault('requires', self.requires) - kwargs.setdefault('skips', self.skips) - kwargs.setdefault('arg_getters', self.arg_getters) - kwargs.setdefault('kwarg_getters', self.kwarg_getters) - kwargs.setdefault('callbacks', self.callbacks) - kwargs.setdefault('available', self._available) - kwargs.setdefault('parent', self.parent) - return self.__class__(**kwargs) - - def ready(self, ctx): - - if self.requires is None: - return True - - for fn in self.requires: - if not fn(ctx): - return False - - return True - - def skip(self, ctx): - - if self.skips is None: - return False - - for fn in self.skips: - if fn(ctx): - return True - - return False - - def get_params(self, ctx): - - args, kwargs = [], {} - - for getter in self.arg_getters: - args.append(getter(ctx)) - - for name, getter in self.kwarg_getters.items(): - if name == 'kwargs': - more = getter(ctx) - kwargs.update(more) - else: - kwargs[name] = getter(ctx) - - return tuple(args), kwargs - - def run_callbacks(self, ctx, result): - - for callback in self.callbacks: - callback(ctx, result) - - def available(self, ctx): - - if callable(self._available): - return self._available(ctx) - - return self._available - - def request(self, ctx=None, args=None, kwargs=None): - return self.request_cls(self, ctx, args, kwargs) - - -class AsyncTask(Task): - - request_cls = AsyncRequest - - -class TaskCollection(object): - - identifier = None - - def __init__(self, identifier=None, tasks=None): - self.identifier = identifier or self.identifier - self.tasks = [] - - # Bind tasks - if tasks: - if isinstance(tasks, (list, tuple, set, self.__class__)): - for task in tasks: - self._bind_task(task) - else: - raise ValueError('Tasks must be a list of task objects') - - # Bind classmethod tasks - for name, member in inspect.getmembers(self.__class__): - if isinstance(member, Task): - bound_task = getattr(self, name) - self.tasks.append(bound_task) - - def __iter__(self): - return iter(self.tasks) - - def __len__(self): - return len(self.tasks) - - def __getitem__(self, index): - return self.tasks.__getitem__(index) - - def __contains__(self, task): - if task in self.tasks: - return True - - for t in self.tasks: - if t.identifier == task.identifier: - return True - - return False - - def index(self, task): - for i, bound_task in enumerate(self.tasks): - if bound_task.identifier == task.identifier: - return i - raise ValueError('%s is not in TaskCollection' % task.identifier) - - def sort(self): - '''Sort tasks by priority''' - - self.tasks.sort(key=lambda t: t.priority) - - def _bind_task(self, task): - '''Bind and add task to this collection''' - - if task in self: - return - - bind_name = task.func_name - if hasattr(self, bind_name): - raise NameError( - 'TaskCollection already has a method %s' % bind_name - ) - - if task.parent is not self: - task = task.clone(parent=self) - setattr(self, bind_name, task) - - self.tasks.append(task) - - def _unbind_task(self, task): - '''Unbind and remove a task from this collection''' - - try: - index = self.index(task) - task = self.tasks.pop(index) - bind_name = task.func_name - if getattr(self, bind_name, None) is task: - delattr(self, bind_name) - except ValueError: - pass - - def append(self, task): - '''Bind and add a task to this collection''' - - self._bind_task(task) - - def remove(self, task): - '''Remove a task from this collection''' - - self._unbind_task(task) - - def extend(self, tasks): - '''Bind and add a list of tasks to this collection''' - - for task in tasks: - self._bind_task(task) - - -# Decorator interface - - -def task(identifier=None, priority=None, description=None, cls=Task): - '''Decorator that converts a function to a Task object. The function - remains callable by normal means, but also receives the additional - functionality of a Task object. - - Arguments: - identifier (str or callable): When a string is passed, return - a decorator which passes the string to the Task object as it's - identifier. If a callable is passed, use the callables name as - the Task's identifier. - priority (Priority or int): Priority used to order tasks in an action - description (str): Defaults to the decorated functions docstring - cls (Object): Task type to create, defaults to Task - - Returns: - Task or instance of cls - ''' - - def make_task(fn, identifier): - return cls( - fn, - identifier=identifier or get_callable_name(fn), - description=description or fn.__doc__, - requires=pop_attr(fn, '__task_requires__', None), - skips=pop_attr(fn, '__task_skips__', None), - arg_getters=pop_attr(fn, '__task_arg_getters__', None), - kwarg_getters=pop_attr(fn, '__task_kwarg_getters__', None), - callbacks=pop_attr(fn, '__task_callbacks__', None), - available=pop_attr(fn, '__task_available__', True), - priority=priority if priority is not None else DEFAULT_PRIORITY - ) - - if callable(identifier): - fn = identifier - identifier = get_callable_name(fn) - return make_task(fn, identifier) - - if isinstance(identifier, basestring) or identifier is None: - def wrapper(fn): - return make_task(fn, identifier) - return wrapper - - raise TypeError("Argument must be: str or callable") - - -def async_task(identifier, priority=None, description=None, cls=AsyncTask): - '''Like :func:`task` decorator. This returns an AsyncTask which will be run - in a separate thread when an Action is evaluated. Can still be called - normally like a standard function. - - Arguments: - identifier (str or callable): When a string is passed, return - a decorator which passes the string to the Task object as it's - identifier. If a callable is passed, use the callables name as - the Task's identifier. - priority (Priority or int): Priority used to order tasks in an action - description (str): Defaults to the decorated functions docstring - cls (Object): Task type to create, defaults to AsyncTask - - Returns: - AsyncTask or instance of cls - ''' - - return task(identifier, priority, description, cls) - - -def requires(*funcs): - '''Task decorator describing a tasks's requirements. - - Each requirement must be a callable accepting ctx as an argument. Once all - requirements are met, the task is ready to run. The ActionRunner will move - the task from the waiting queue to the ready queue and execute it in the - next loop. - - Require the success of another task: - >>> @task - ... @requires(success('other.task')) - ... def my_task(): - ... pass - - Require failure of another task: - >>> @task - ... @requires(failure('other.task')) - ... def my_task(): - ... pass - ''' - - def requires(fn): - if not hasattr(fn, '__task_requires__'): - fn.__task_requires__ = [] - fn.__task_requires__.extend([_requires_for('requires', f) for f in funcs]) - return fn - - return requires - - -def skips(*funcs): - - def skips(fn): - if not hasattr(fn, '__task_skips__'): - fn.__task_skips__ = [] - fn.__task_skips__.extend([_getter_for('skips', f) for f in funcs]) - return fn - - return skips - - -def pass_context(fn): - '''Task decorator that passes Context object as first argument to task''' - - def pass_ctx(ctx): - return ctx - - if not hasattr(fn, '__task_arg_getters__'): - fn.__task_arg_getters__ = [] - - if len(fn.__task_arg_getters__): - for getter in fn.__task_arg_getters__: - if getattr(getter, '__name__', None) == 'pass_ctx': - return fn - - fn.__task_arg_getters__.insert(0, pass_ctx) - return fn - - -def pass_args(fn): - '''Task decorator that passes Context object as first argument to task''' - - def pass_args(ctx): - return ctx.args - - if not hasattr(fn, '__task_arg_getters__'): - fn.__task_arg_getters__ = [] - - if len(fn.__task_arg_getters__): - for getter in fn.__task_arg_getters__: - if getattr(getter, '__name__', None) == 'pass_args': - return fn - - fn.__task_arg_getters__.insert(0, pass_args) - return fn - - -def pass_kwargs(fn): - '''Task decorator that passes all action kwargs to the task''' - - def pass_kwargs(ctx): - return ctx.kwargs - - if not hasattr(fn, '__task_kwarg_getters__'): - fn.__task_kwarg_getters__ = {} - fn.__task_kwarg_getters__['kwargs'] = pass_kwargs - return fn - - -def params(*args, **kwargs): - '''Task decorator used to describe how to get args and kwargs to pass to - the task. This should have the same signature as the task itself, but, - each argument and keyword argument should be a function accepting one - argument, ctx, that returns a value to pass to the task. - - Examples: - - >>> @task - ... @params(store('a'), b=store('b')) - ... def plus(a, b): - ... return a + b - - # pass_context provides ctx, so we describe the remaining args - >>> @task - ... @pass_context - ... @params(store('a'), store('b')) - ... def plus2(ctx, a, b): - ... value = a + b - ... ctx.store['plus_result'] = value - ... return value - - ''' - - def describe_params(fn): - if not hasattr(fn, '__task_arg_getters__'): - fn.__task_arg_getters__ = [] - - for arg in args: - fn.__task_arg_getters__.append(_getter_for('params', arg)) - - if not hasattr(fn, '__task_kwarg_getters__'): - fn.__task_kwarg_getters__ = {} - - for name, arg in kwargs.items(): - fn.__task_kwarg_getters__[name] = _getter_for('params', arg) - - return fn - - return describe_params - - -def returns(*callbacks): - '''Task decorator that adds callbacks to run when the task successfully - returns. Also accepts CtxAction objects like artifact(key) and store(key) - ''' - - def returns(fn): - if not hasattr(fn, '__task_callbacks__'): - fn.__task_callbacks__ = [] - - for callback in callbacks: - fn.__task_callbacks__.append(_setter_for('returns', callback)) - - return fn - - return returns - - -def available(value): - '''Task available when value is True or value() is True''' - - def available(fn): - fn.__task_available__ = value - return fn - - return available - - -def success(identifier): - '''Check if a task succeeded''' - - def success(ctx): - '''Returns True if the task has succeeded''' - try: - request = ctx.requests[identifier] - return request.success - except KeyError: - raise TaskError('success(%r): Request not found..' % identifier) - - return success - - -def done(identifier): - '''Check if a task is done''' - - def done(ctx): - '''Returns True if the task is done''' - try: - request = ctx.requests[identifier] - return request.done - except KeyError: - raise TaskError('done(%r): Request not found..' % identifier) - - return done - - -def failure(identifier): - '''Check if a task failed''' - - def failure(ctx): - '''Returns True if the task has failed''' - try: - request = ctx.requests[identifier] - return request.failed - except KeyError: - raise TaskError('failure(%r): Request not found..' % identifier) - - return failure - - -class CtxAction(object): - - section = '' - sep = ':' - actions = 'store', 'append', 'count' - gets = True - sets = True - requires = True - - def __init__(self, key, action='store'): - self.key = key - self.action = action - - def split_key(self): - if self.sep in self.key: - keys = self.key.split() - return keys[:-1], keys[-1] - return [], self.key - - def get_section_key(self, ctx): - keys, key = self.split_key() - section = getattr(ctx, self.section) - if keys: - for key in keys: - section = section.get(key) - return section, key - - def exists(self, ctx): - try: - section, key = self.get_section_key(ctx) - except KeyError: - return False - return key in section - - def get(self, ctx): - section, key = self.get_section_key(ctx) - return section[key] - - def set(self, ctx, value): - section, key = self.get_section_key(ctx) - if self.action == 'store': - section[key] = value - elif self.action == 'append': - if key not in section: - section[key] = [] - section[key].append(value) - else: - if key in section and type(section[key], (float, int)): - section[key] += value - - -class artifact(CtxAction): - '''Use as an argument for :func:`params` or :func:`returns`. - When used with :func:`params`, artifact will get the value of the key - from the context's "artifacts" dict and supply it as an arg or kwarg for - the task. If used with :func:`returns`, artifact will inject the return - value of the task into the specified key of the context's "artifacts" dict. - You can specify a nested key using : as a separator. - - Examples: - - # Set ctx.artifacts['funky'] to return value of task - >>> @task - ... @returns(artifact('funky')) - ... def funky_func(): - ... return {} - - # Set ctx.artifacts['funky']['monkey'] to return value of task - >>> @task - ... @returns(artifact('funky:monkey')) - ... def funky_monkey_func(): - ... return 'Super Funky Monkey' - ''' - section = 'artifacts' - - -class store(CtxAction): - '''Use as an argument for :func:`params` or :func:`returns`. - When used with :func:`params`, store will get the value of the key - from the context's "store" dict and supply it as an arg or kwarg for - the task. If used with :func:`returns`, store will inject the return - value of the task into the specified key of the context's "store" dict. - You can specify a nested key using : as a separator. - - Examples: - - # Pass ctx.store['x'] as an arg and ctx.store['y'] as kwarg to add - # Set ctx.store['sum'] to the return value of add - >>> @task - ... @params(store('x'), b=store('y')) - ... @returns(store('sum')) - ... def add(a, b): - ... return a + b - ''' - section = 'store' - - -class kwarg(CtxAction): - '''Use as an argument for :func:`params`. When used with :func:`params`, - kwarg will get the value of the key from the context's "kwargs" dict and - supply it as an arg or kwarg for the task. This action can not be used with - :func:`returns`. - - You can specify a nested key using : as a separator. - - Examples: - - # Pass ctx.store['x'] as an arg and ctx.store['y'] as kwarg to add - # Set ctx.store['sum'] to the return value of add - >>> @task - ... @params(store('x'), b=store('y')) - ... @returns(store('sum')) - ... def add(a, b): - ... return a + b - ''' - section = 'kwargs' - - def set(self, ctx, value): - raise NotImplementedError('Can not use kwarg with the returns deco') - - -def _requires_for(name, obj): - '''Returns a callable object for use with task decorators. Requirement - must accept one argument ctx.''' - - if callable(obj): - spec = inspect.getargspec(obj) - if spec.args and len(spec.args) == 1: - return obj - - raise TypeError('%s must accept ctx as an argument' % name) - - if isinstance(obj, CtxAction): - if obj.requires: - return obj.exists - - raise TypeError( - '%s can not be used as an argument to %s()' % - (type(obj).__name__, name) - ) - - raise TypeError( - '%s received unsupported type: %s\nMust be a callable or CtxAction' % - (type(obj).__name__, name) - ) - - -def _getter_for(name, obj): - '''Returns a callable object for use with task decorators. Getters - must accept one argument ctx.''' - - if callable(obj): - spec = inspect.getargspec(obj) - if spec.args and len(spec.args) == 1: - return obj - - raise TypeError('%s must accept ctx as an argument' % name) - - if isinstance(obj, CtxAction): - if obj.gets: - return obj.get - - raise TypeError( - '%s can not be used as an argument to %s()...' % - (type(obj).__name__, name) - ) - - raise TypeError( - '%s received unsupported type: %s\nMust be a callable or CtxAction' % - (type(obj).__name__, name) - ) - - -def _setter_for(name, obj): - '''Returns a callable object for use with task decorators. Setters - must accept two arguments ctx and value.''' - - if callable(obj): - spec = inspect.getargspec(obj) - if spec.args and len(spec.args) == 2: - return obj - - raise TypeError('%s must accept two args: ctx and value' % name) - - if isinstance(obj, CtxAction): - if obj.sets: - return obj.set - - raise TypeError( - '%s can not be used as an argument to %s()...' % - (type(obj).__name__, name) - ) - - raise TypeError( - '%s received unsupported type: %s\nMust be a callable or CtxAction' % - (type(obj).__name__, name) - ) - - -# Sort methods - -def sort_tasks(tasks): - '''Sort the given tasks by priority''' - - return sorted(tasks, key=attrgetter('priority')) - - -def group_tasks(tasks): - '''Group sorted tasks by priority''' - - groups = OrderedDict() - for task in tasks: - groups.setdefault(task.priority, []) - groups[task.priority].append(task) - return groups diff --git a/construct/tests/__init__.py b/construct/tests/__init__.py deleted file mode 100644 index 8ea9a79..0000000 --- a/construct/tests/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -import os -from functools import partial - - -# Convenient attributes and functions -root = os.path.dirname(__file__) -data = os.path.join(root, 'test_data') -test_path = partial(os.path.join, root) -data_path = partial(os.path.join, data) diff --git a/construct/tests/test_actionparams.py b/construct/tests/test_actionparams.py deleted file mode 100644 index f73eb30..0000000 --- a/construct/tests/test_actionparams.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function -from nose.tools import raises -from construct import actionparams -from construct.errors import ArgumentError - - -params_0 = dict() -params_1 = dict( - str_arg={ - 'label': 'String Argument', - 'help': 'A String Argument', - 'required': True, - 'type': str - }, - int_arg={ - 'label': 'Integer Argument', - 'help': 'An Integer Argument', - 'required': True, - 'default': 1, - 'type': int - }, - float_arg={ - 'label': 'Float Argument', - 'help': 'A Float Argument', - 'required': False, - 'default': 10.0, - 'type': float - }, - dict_arg={ - 'label': 'Dict Argument', - 'help': 'A Dict Argument', - 'required': True, - 'type': dict - } -) - - -def test_validate_nada(): - '''Validate empty params dict''' - actionparams.validate(params_0) - actionparams.validate(params_1) - - -@raises(ArgumentError) -def test_pass_args_to_empty_params(): - '''Validate kwargs against empty params''' - actionparams.validate_kwargs(params_0, {'invalid': 'kwargs'}) - - -@raises(ArgumentError) -def test_missing_required(): - '''Validate kwargs with missing required argument''' - actionparams.validate_kwargs( - params_1, - {'str_arg': 'str', 'dict_arg': {}} - ) diff --git a/construct/tests/test_data/extpath1/extensiona.py b/construct/tests/test_data/extpath1/extensiona.py deleted file mode 100644 index 4a8a6d8..0000000 --- a/construct/tests/test_data/extpath1/extensiona.py +++ /dev/null @@ -1,6 +0,0 @@ -from construct import Extension - - -class ExtensionA(Extension): - name = 'ExtensionA' - attr_name = 'extension_a' diff --git a/construct/tests/test_data/extpath2/extensionb.py b/construct/tests/test_data/extpath2/extensionb.py deleted file mode 100644 index e465a9f..0000000 --- a/construct/tests/test_data/extpath2/extensionb.py +++ /dev/null @@ -1,6 +0,0 @@ -from construct import Extension - - -class ExtensionB(Extension): - name = 'ExtensionB' - attr_name = 'extension_b' diff --git a/construct/tests/test_data/extpath3/extension_package/__init__.py b/construct/tests/test_data/extpath3/extension_package/__init__.py deleted file mode 100644 index 249d546..0000000 --- a/construct/tests/test_data/extpath3/extension_package/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from construct import Extension - - -class ExtensionC(Extension): - name = 'ExtensionC' - attr_name = 'extension_c' diff --git a/construct/tests/test_extensions.py b/construct/tests/test_extensions.py deleted file mode 100644 index 70fd34c..0000000 --- a/construct/tests/test_extensions.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function -from construct.tests import data_path -import construct - - -def teardown(): - construct.extensions.clear() - - -def test_discover_single_path(): - '''Discover Extensions on single path''' - - construct.extensions.discover(data_path('extpath1')) - exts = construct.extensions.collect() - assert 'ExtensionA' in exts - - -def test_discover_multiple_paths(): - '''Discover Extensions on multiple paths''' - - construct.extensions.discover( - data_path('extpath1'), - data_path('extpath2'), - data_path('extpath3'), - ) - exts = construct.extensions.collect() - assert 'ExtensionA' in exts - assert 'ExtensionB' in exts - assert 'ExtensionC' in exts diff --git a/construct/tests/test_tasks.py b/construct/tests/test_tasks.py deleted file mode 100644 index a1c699e..0000000 --- a/construct/tests/test_tasks.py +++ /dev/null @@ -1,273 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function -import time -from timeit import default_timer -from construct.tasks import Task, AsyncTask, task, TaskCollection -from construct.errors import TimeoutError -from construct.constants import * -from nose.tools import raises - - -def func_no_args(): - return 'SUCCESS' - - -def func_w_args(*args, **kwargs): - return args, kwargs - - -def func_poll(interval): - time.sleep(interval) - return True - - -def func_raises(): - raise ValueError - - -def test_call_Task(): - '''Call a Task''' - - t = Task(func_no_args, 'func.no.args', 'function without arguments') - assert t() == 'SUCCESS' - - t = Task(func_w_args, 'func.w.args', 'function with arguments') - args, kwargs = ('arg',), {'kwarg': None} - assert t(*args, **kwargs) == (args, kwargs) - - -def test_call_AsyncTask(): - '''Call an AsyncTask''' - - t = AsyncTask(func_no_args, 'func.no.args', 'function without arguments') - assert t() == 'SUCCESS' - - t = AsyncTask(func_w_args, 'func.w.args', 'function with arguments') - args, kwargs = ('arg',), {'kwarg': None} - assert t(*args, **kwargs) == (args, kwargs) - - -def test_Task_request(): - '''Use a task request''' - - t = Task(func_no_args, 'func.no.args', 'function without arguments') - r = t.request() - - assert r.task is t - assert r.args == () - assert r.kwargs == {} - assert r.status == WAITING - assert not r.ready - assert r.get() == 'SUCCESS' - assert r.ready - assert r.status == SUCCESS - - -@raises(ValueError) -def test_Request_propagate(): - '''Request exception propagation''' - - t = Task(func_raises, 'func.raises', 'Task raises ValueError') - - # Do not propagate - r = t.request() - exc = r.get(propagate=False) - assert r.status == FAILED - assert exc[0] == ValueError - - # Propagate - r = t.request() - r.get() - - -def test_AsyncTask_request(): - '''Use an async task request''' - - t = AsyncTask(func_poll, 'func.poll', 'Polling task') - r = t.request(args=(0,)) - - assert r.task is t - assert r.args == (0,) - assert r.kwargs == {} - assert r.status == WAITING - assert not r.ready - assert r.get() - assert r.ready - assert r.status == SUCCESS - - -@raises(TimeoutError) -def test_AsyncRequest_timeout(): - '''AsyncRequest raises TimeoutError''' - - t = AsyncTask(func_poll, 'func.poll', 'Polling task') - r = t.request(args=(1,)) - r.get(0) - - -@raises(ValueError) -def test_AsyncRequest_propagate(): - '''AsyncRequest exception propagation''' - - t = AsyncTask(func_raises, 'func.raises', 'Task raises ValueError') - - # Do not propagate - r = t.request() - exc = r.get(propagate=False) - assert r.status == FAILED - assert exc[0] == ValueError - - # Propagate - r = t.request() - r.get() - - -def test_AsyncRequest_returns_asap(): - '''AsyncRequest get returns ASAP''' - - t = AsyncTask(func_poll, 'func.poll', 'Polling task') - r = t.request(args=(0.1,)) - - st = default_timer() - value = r.get(1) - - assert default_timer() - st < 1 - assert value - - -def test_task_decorator(): - '''Task function decorator''' - - @task - def null_task(): - return True - - assert not null_task.bound - assert null_task.identifier == 'null_task' - assert null_task.priority == 0 - assert null_task() - assert null_task.ready(None) - - r = null_task.request() - assert r.get() - - -def test_task_method(): - '''Task method decorator''' - - class ObjectWithTasks(object): - - @task(priority=0) - def task_a(self): - return True - - tc = ObjectWithTasks() - - assert tc.task_a is tc.task_a - assert tc.task_a.bound - assert tc.task_a.identifier == 'task_a' - assert tc.task_a() - - tc2 = ObjectWithTasks() - - assert tc.task_a is not tc2.task_a - - -@task -def null_task(): - return True - - -class TestCollection(TaskCollection): - - identifier = 'test_collection' - - @task(priority=0) - def task_a(self): - return True - - @task(priority=1) - def task_b(self): - return False - - @task(priority=2) - def task_c(self): - return False - - -def test_task_collection_methods(): - '''TaskCollection automagical task registration''' - - tc = TestCollection() - - # Membership tests use task identifiers - assert TestCollection.task_a in tc - assert TestCollection.task_b in tc - assert TestCollection.task_c in tc - assert tc.task_a() - assert not tc.task_b() - assert not tc.task_c() - - # All task methods are automagically bound in __init__ - assert tc.task_a.bound and tc.task_a.parent is tc - assert tc.task_b.bound and tc.task_b.parent is tc - assert tc.task_c.bound and tc.task_c.parent is tc - assert len(tc) == 3 - - -def test_task_collection_add_remove(): - '''TaskCollection append and remove''' - - tc = TestCollection() - - # Add a task - tc.append(null_task) - assert null_task in tc - assert len(tc) == 4 - assert hasattr(tc, null_task.func_name) - assert tc.null_task() - - # Does nothing - tc.append(null_task) - assert len(tc) == 4 - - # Remove a task - tc.remove(null_task) - assert null_task not in tc - assert len(tc) == 3 - assert not hasattr(tc, null_task.func_name) - - # Does nothing - tc.remove(null_task) - assert len(tc) == 3 - - -def test_task_collection_init_args(): - '''TaskCollection init args''' - - @task(priority=0) - def task_a(): - return True - - @task(priority=1) - def task_b(): - return False - - @task(priority=2) - def task_c(): - return False - - tc = TaskCollection('identifier', [task_a, task_b, task_c]) - assert tc.identifier == 'identifier' - assert len(tc) == 3 - assert tc.task_a() - assert not tc.task_b() - assert not tc.task_c() - - tc.remove(task_a) - tc.remove(task_b) - tc.remove(task_c) - assert len(tc) == 0 - assert not hasattr(tc, task_a.func_name) - assert not hasattr(tc, task_b.func_name) - assert not hasattr(tc, task_c.func_name) diff --git a/construct/tests/test_types.py b/construct/tests/test_types.py deleted file mode 100644 index 87ebb1c..0000000 --- a/construct/tests/test_types.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from construct import types - - -def test_weakset_bound_methods(): - '''Weakset with bound methods''' - - class Object(object): - - def method_a(self): - pass - - def method_b(self): - pass - - weakset = types.weakset() - - a = Object() - b = Object() - - weakset.add(a.method_a) - weakset.add(a.method_b) - weakset.add(b.method_a) - weakset.add(b.method_b) - assert len(weakset) == 4 - - del(b) - assert len(weakset) == 2 - - weakset.add(a.method_a) - weakset.add(a.method_b) - assert len(weakset) == 2 - - del(a) - assert len(weakset) == 0 - - -def test_weakset_functions(): - '''Weakset with functions''' - - weakset = types.weakset() - - def func(): - pass - - weakset.add(func) - assert len(weakset) == 1 - - weakset.add(func) - assert len(weakset) == 1 - - del(func) - assert len(weakset) == 0 diff --git a/construct/tests/utils.py b/construct/tests/utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/construct/types.py b/construct/types.py index fd60986..0f093ea 100644 --- a/construct/types.py +++ b/construct/types.py @@ -1,227 +1,113 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -__all__ = [ - 'ABC', - 'Namespace', - 'Stack', - 'Priority', - 'STAGE0', - 'STAGE1', - 'STAGE2', - 'STAGE3', - 'STAGE4', - 'STAGE', - 'VALIDATE', - 'REPAIR', - 'COMMIT', - 'INTEGRATE', - 'weakset', - 'weakmeth', -] - -import abc -import inspect +# Standard library imports import weakref -from collections import Mapping, deque -from construct.utils import update_dict - -# Use for type checking with isinstance -# Bring this in for api convenience -from construct.models import Entry -Number = (int, float) - -# Alias some six types -import six -Binary = six.binary_type -Integer = six.integer_types -Object = six.class_types -Text = six.text_type -if six.PY2: - String = (str, unicode) -else: - String = str - -# py2-3 compatible ABCMeta -ABC = abc.ABCMeta('ABC', (object,), {}) - - -class Namespace(object): - - def __init__(self, **kwargs): - self.__dict__.update(**kwargs) - - def __repr__(self): - return '{}({})'.format( - self.__class__.__name__, - ', '.join( - [('{}={}').format(k, v) for k, v in self.__dict__.items()] - ) - ) - - def __contains__(self, key): - return key in self.__dict__ - - def __getitem__(self, key): - return self.__dict__.__getitem__(key) - - def __setitem__(self, key, value): - return self.__dict__.__setitem__(key, value) - - def items(self): - return self.__dict__.items() - - def values(self): - return self.__dict__.values() - - def keys(self): - return self.__dict__.keys() - - def update(self, other): - if isinstance(other, Namespace): - update_dict(self.__dict__, other.__dict__) - elif isinstance(other, Mapping): - update_dict(self.__dict__, other) - else: - raise TypeError('Argument must be a Namespace or Mapping instance') - - -class Stack(deque): - def push(self, item): - self.appendleft(item) - def size(self): - return len(self) +class WeakRef(weakref.ref): + '''Same as weakref.ref but supports attribute assignment.''' - def empty(self): - return not self - def top(self): - return self[-1] +class WeakMeth(object): + '''weakref.ref for methods.''' + def __init__(self, obj, callback=None): + self.name = obj.__name__ + self.ref = WeakRef(obj.__self__, callback) -# Priority Type and Defaults - - -class Priority(int): - - _instances = {} - - def __new__(cls, value, label=None, description=None): - - key = (value, label, description) - if key in cls._instances: - return cls._instances[key] - - if label is None: - label = 'Priority-' + str(value) - - if description is None: - description = 'Priority ' + str(value) - - key = (value, label, description) - if key in cls._instances: - return cls._instances[key] + def __call__(self): + inst = self.ref() + if inst is None: + return + return getattr(inst, self.name) - obj = super(Priority, cls).__new__(cls, value) - obj.label = label - obj.description = description - return cls._instances.setdefault(key, obj) +class StrongRef(object): + '''Hold a reference to an object.''' + def __init__(self, obj, callback=None): + self.name = obj.__name__ + self.obj = obj -STAGE0 = Priority(0, 'Stage-0', 'First stage') -STAGE1 = Priority(1, 'Stage-1', 'Second stage') -STAGE2 = Priority(2, 'Stage-2', 'Third stage') -STAGE3 = Priority(3, 'Stage-3', 'Fourth stage') -STAGE4 = Priority(4, 'Stage-4', 'Fifth stage') + def __call__(self): + return self.obj -STAGE = Priority(0, 'Stage', 'Stage') -VALIDATE = Priority(1, 'Validate', 'Validate') -REPAIR = Priority(2, 'Repair', 'Repair') -COMMIT = Priority(3, 'Commit', 'Commit') -INTEGRATE = Priority(4, 'Integrate', 'Integrate artifacts') -DEFAULTS = { - (0, None, None): STAGE0, - (1, None, None): STAGE1, - (2, None, None): STAGE2, - (3, None, None): STAGE3, - (4, None, None): STAGE4, -} -Priority._instances.update(DEFAULTS) +class WeakSet(object): + '''A weakset implementation that supports methods. The underlying + data is stored as list so it will remain ordered unlike a true set. + ''' + def __init__(self, iterator=None): + self._refs = [] + self._ids = [] -class weakset(object): - '''Like WeakSet but works with bound methods''' + if iterator is not None: + for item in iterator: + self.add(item) - def __init__(self): - self._ids = [] - self._refs = [] + def __len__(self): + return len(self._ids) - def __repr__(self): + def __contains__(self, obj): return ( - '<{}>(_ids={}, _refs={})' - ).format( - self.__class__.__name__, - self._ids, - self._refs + obj in self._ids or + obj in self._refs or + self._ref_id(obj) in self._ids ) - def __len__(self): - return len(self._refs) - def __iter__(self): - for i in range(len(self._refs)): - id_ = self._ids.pop(0) - ref = self._refs.pop(0) + for ref in self._refs: obj = ref() - if obj is not None: - yield obj - self._refs.append(ref) - self._ids.append(id_) + if obj is None: + continue + yield obj + + def _ref_id(self, obj): + if is_method(obj): + if hasattr(obj, '__func__'): + return id(obj.__self__), id(obj.__func__) + return id(obj.__self__), id(obj.__name__) + else: + return id(obj) - def discard(self, obj): - if obj not in self._refs: + def _remove_ref(self, ref): + ref_id = ref.ref_id + if ref_id not in self._ids: return - index = self._refs.index(obj) + index = self._ids.index(ref_id) self._ids.pop(index) self._refs.pop(index) - def add(self, obj): - id_ = weak_id(obj) - - if id_ in self._ids: + def add(self, obj, strong=False): + ref_id = self._ref_id(obj) + if ref_id in self._ids: return - self._ids.append(id_) - - if inspect.ismethod(obj): - self._refs.append(weakmeth(obj, self.discard)) + self._ids.append(ref_id) + if strong: + ref = StrongRef(obj) + ref.ref_id = ref_id + elif is_method(obj): + ref = WeakMeth(obj, self._remove_ref) + ref.ref.ref_id = ref_id else: - self._refs.append(weakref.ref(obj, self.discard)) - + ref = WeakRef(obj, self._remove_ref) + ref.ref_id = ref_id -def weak_id(obj): - '''Returns a unique and constant id for an object including bound meths.''' - - if inspect.ismethod(obj): - return id(obj.__self__) + id(obj.__func__) - else: - return id(obj) + self._refs.append(ref) + def discard(self, obj): + ref_id = self._ref_id(obj) + if ref_id not in self._ids: + return -class weakmeth(object): - '''Like weakref but for bound methods''' + index = self._ids.index(ref_id) + self._ids.pop(index) + self._refs.pop(index) - def __init__(self, meth, callback=None): - self.name = meth.__name__ - self.ref = weakref.ref(meth.__self__, lambda _: callback(self)) - def __call__(self): - obj = self.ref() - if obj is None: - return - return getattr(obj, self.name, None) +def is_method(obj): + return hasattr(obj, '__call__') and hasattr(obj, '__self__') diff --git a/construct/builtins/templates/vfx_project/assets/prop/.data/data b/construct/ui/__init__.py similarity index 100% rename from construct/builtins/templates/vfx_project/assets/prop/.data/data rename to construct/ui/__init__.py diff --git a/construct/ui/dialogs.py b/construct/ui/dialogs.py new file mode 100644 index 0000000..0adc272 --- /dev/null +++ b/construct/ui/dialogs.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Third party imports +from Qt import QtCore, QtGui, QtWidgets + +# Local imports +from .layouts import HBarLayout +from .scale import pt +from .theme import theme +from .widgets import H3, P + + +class FramelessDialog(QtWidgets.QDialog): + '''Frameless Dialog + + Arguments: + parent (QObject) + f (QtCore.Qt.WindowFlags) + ''' + + _resize_area_map = { + (False, False, False, False): None, + (True, False, False, False): 'left', + (True, True, False, False): 'topLeft', + (False, True, False, False): 'top', + (False, True, True, False): 'topRight', + (False, False, True, False): 'right', + (False, False, True, True): 'bottomRight', + (False, False, False, True): 'bottom', + (True, False, False, True): 'bottomLeft' + } + _cursor_map = { + None: QtCore.Qt.ArrowCursor, + 'left': QtCore.Qt.SizeHorCursor, + 'topLeft': QtCore.Qt.SizeFDiagCursor, + 'top': QtCore.Qt.SizeVerCursor, + 'topRight': QtCore.Qt.SizeBDiagCursor, + 'right': QtCore.Qt.SizeHorCursor, + 'bottomRight': QtCore.Qt.SizeFDiagCursor, + 'bottom': QtCore.Qt.SizeVerCursor, + 'bottomLeft': QtCore.Qt.SizeBDiagCursor + } + css_id = 'surface' + + def __init__(self, parent=None): + super(FramelessDialog, self).__init__(parent=parent) + + self._mouse_pressed = False + self._mouse_position = None + self._resize_area = None + self.resize_area_size = pt(5) + self.setMouseTracking(True) + self.setWindowFlags( + QtCore.Qt.Dialog | + QtCore.Qt.WindowStaysOnTopHint | + QtCore.Qt.FramelessWindowHint + ) + self.setObjectName(self.css_id) + self.setWindowTitle('construct') + self.setWindowIcon(theme.icon('brand/construct_icon-white.png')) + theme.apply(self) + + margins = (pt(10), pt(10), pt(10), pt(10)) + + self.header = QtWidgets.QWidget() + self.header_layout = HBarLayout() + self.header.setLayout(self.header_layout) + + self.body = QtWidgets.QWidget() + self.body_layout = QtWidgets.QVBoxLayout() + self.body_layout.setContentsMargins(0, 0, 0, 0) + self.body_layout.setSpacing(pt(10)) + self.body.setLayout(self.body_layout) + + self.footer = QtWidgets.QWidget() + self.footer_layout = HBarLayout() + self.footer.setLayout(self.footer_layout) + + self.layout = QtWidgets.QVBoxLayout() + self.layout.setContentsMargins(*margins) + self.layout.setSpacing(pt(8)) + self.layout.addWidget(self.header) + self.layout.addWidget(self.body) + self.layout.addWidget(self.footer) + self.setLayout(self.layout) + + @property + def resizing(self): + return bool(self._resize_area) + + def _check_resize_area(self, pos): + + x, y = pos.x(), pos.y() + self._resize_area = self._resize_area_map[( + x < self.resize_area_size, + y < self.resize_area_size, + x > self.width() - self.resize_area_size, + y > self.height() - self.resize_area_size, + )] + + def mousePressEvent(self, event): + + if event.buttons() & QtCore.Qt.LeftButton: + pos = event.pos() + self._check_resize_area(pos) + self._mouse_pressed = True + self._mouse_position = pos + + def mouseMoveEvent(self, event): + + if not self._mouse_pressed: + pos = event.pos() + self._check_resize_area(pos) + cursor = self._cursor_map.get(self._resize_area) + self.setCursor(cursor) + + if self._mouse_pressed: + vector = event.pos() - self._mouse_position + offset = event.globalPos() + + if self.resizing: + min_width = self.minimumWidth() + min_height = self.minimumHeight() + rect = self.geometry() + resize_area = self._resize_area.lower() + + if 'left' in resize_area: + new_width = rect.width() - vector.x() + if new_width > min_width: + rect.setLeft(offset.x()) + + if 'right' in resize_area: + new_width = rect.width() + vector.x() + if new_width > min_width: + rect.setRight(offset.x()) + + if 'top' in resize_area: + new_height = rect.height() - vector.y() + if new_height > min_height: + rect.setTop(offset.y()) + + if 'bottom' in resize_area: + new_height = rect.height() + vector.y() + if new_height > min_height: + rect.setBottom(offset.y()) + + self.setGeometry(rect) + + else: + self.move(self.mapToParent(vector)) + + def mouseReleaseEvent(self, event): + self._mouse_pressed = False + self._mouse_position = None + + +class Notification(FramelessDialog): + + def __init__( + self, + type, + message, + title=None, + icon=None, + close_icon=None, + short=None, + parent=None, + ): + super(Notification, self).__init__(parent=parent) + + self.setObjectName(type.lower()) + self.setMinimumWidth(pt(272)) + + self.header_message = P(message, parent=self) + self.header_message.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter + ) + self.body_message = P(message, parent=self) + self.body_message.setAlignment(QtCore.Qt.AlignLeft) + self.title = H3(title or type.title(), parent=self) + self.title.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter + ) + + # TODO: Creat icon widget + self.icon_widget = QtWidgets.QPushButton(parent=self) + self.icon_widget.hide() + self.icon_widget.setObjectName('icon') + self.icon_widget.setFlat(True) + self.icon_widget.setDisabled(True) + self.set_icon(icon) + + self.close_button = QtWidgets.QPushButton(parent=self) + self.close_button.setObjectName('icon') + self.close_button.setFlat(True) + self.close_button.clicked.connect(self.accept) + self.set_close_icon(close_icon) + + self.header_layout.left.addWidget(self.icon_widget) + self.header_layout.center.addWidget(self.header_message, stretch=1) + self.header_layout.center.addWidget(self.title, stretch=1) + self.header_layout.right.addWidget(self.close_button) + self.body_layout.addWidget(self.body_message, stretch=1) + + if short is not None: + self.set_short(short) + else: + self.set_short(len(message) < 72) + + def set_short(self, value): + self.is_short = value + if self.is_short: + self.header_message.show() + self.title.hide() + self.body.hide() + self.footer.hide() + self.adjustSize() + else: + self.title.show() + self.footer.setVisible(self.footer_layout.count()) + self.body.show() + self.header_message.hide() + self.adjustSize() + + def set_type(self, type): + self.setObjectName(type.lower()) + theme.apply(self) + + def set_icon(self, icon): + if icon: + self.icon = theme.icon(icon, parent=self.icon_widget) + self.icon_widget.setIcon(self.icon) + self.icon_widget.setIconSize(QtCore.QSize(pt(24), pt(24))) + self.icon_widget.show() + self.title.setAlignment(QtCore.Qt.AlignCenter) + else: + self.icon = None + self.icon_widget.hide() + self.header_message.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter + ) + self.title.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter + ) + + def set_close_icon(self, icon): + self.close_icon = theme.icon( + icon or 'close', + parent=self.close_button + ) + self.close_button.setIcon(self.close_icon) + self.close_button.setIconSize(QtCore.QSize(pt(24), pt(24))) + + +class Ask(FramelessDialog): + + def __init__( + self, + title, + message, + yes_label='Yes', + no_label='No', + icon=None, + parent=None, + ): + super(Ask, self).__init__(parent=parent) + self.setObjectName('surface') + self.setMinimumWidth(pt(272)) + + self.title = H3(title, parent=self) + self.title.setAlignment( + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter + ) + self.body_message = P(message, parent=self) + self.body_message.setAlignment(QtCore.Qt.AlignLeft) + + # TODO: Creat icon widget + self.icon_widget = QtWidgets.QPushButton(parent=self) + self.icon_widget.hide() + self.icon_widget.setObjectName('icon') + self.icon_widget.setFlat(True) + self.icon_widget.setDisabled(True) + self.set_icon(icon) + + self.yes_button = QtWidgets.QPushButton(yes_label, parent=self) + self.yes_button.setObjectName('text-button') + self.yes_button.setFlat(True) + self.yes_button.clicked.connect(self.accept) + + self.no_button = QtWidgets.QPushButton(no_label, parent=self) + self.no_button.setObjectName('text-button') + self.no_button.setFlat(True) + self.no_button.clicked.connect(self.reject) + + self.header_layout.left.addWidget(self.icon_widget) + self.header_layout.center.addWidget(self.title, stretch=1) + self.body_layout.addWidget(self.body_message, stretch=1) + self.footer_layout.right.addWidget(self.yes_button) + self.footer_layout.right.addWidget(self.no_button) + + self.adjustSize() + + def set_icon(self, icon): + if icon: + self.icon = theme.icon(icon, parent=self.icon_widget) + self.icon_widget.setIcon(self.icon) + self.icon_widget.setIconSize(QtCore.QSize(pt(24), pt(24))) + self.icon_widget.show() + else: + self.icon = None + self.icon_widget.hide() diff --git a/construct/ui/eventloop.py b/construct/ui/eventloop.py new file mode 100644 index 0000000..04a4ca3 --- /dev/null +++ b/construct/ui/eventloop.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +from functools import wraps + + +class EventLoop(object): + + _instance = None + + def __init__(self, event_loop, standalone): + self.event_loop = event_loop + self.standalone = standalone + self.running = not self.standalone + + def start(self): + if self.running: + return + + self.running = True + self.event_loop.setQuitOnLastWindowClosed(True) + return self.event_loop.exec_() + + def stop(self): + if not self.running: + return + + self.event_loop.exit() + self.running = False + + +def get_event_loop(): + '''Get the EventLoop instance.''' + + from Qt.QtWidgets import QApplication + + if not EventLoop._instance: + + standalone = False + event_loop = QApplication.instance() + + if not event_loop: + event_loop = QApplication([]) + standalone = True + + EventLoop._instance = EventLoop(event_loop, standalone) + + return EventLoop._instance + + +def requires_event_loop(fn): + '''Make sure the event loop is started before executing the function.''' + + + @wraps(fn) + def start_then_call(*args, **kwargs): + from Qt.QtCore import QTimer + + event_loop = get_event_loop() + result = fn(*args, **kwargs) + return result + + return start_then_call diff --git a/construct/ui/icons.py b/construct/ui/icons.py new file mode 100644 index 0000000..f8c6729 --- /dev/null +++ b/construct/ui/icons.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- + +# Third party imports +from Qt import QtCore +from Qt.QtGui import QColor, QIcon, QIconEngine, QImage, QPainter, QPixmap +from Qt.QtSvg import QSvgRenderer + + +class SvgIconEngine(QIconEngine): + '''Handles painting of SVG icons.''' + + def __init__(self, svg_file, parent=None): + super(SvgIconEngine, self).__init__() + self.svg_file = svg_file + self.svg = QSvgRenderer(self.svg_file) + self.parent = parent + + def pixmap(self, size, mode, state): + img = QImage(size, QImage.Format_ARGB32) + img.fill(QColor(0, 0, 0, 0)) + pm = QPixmap.fromImage(img) + painter = QPainter(pm) + self.paint(painter, pm.rect(), QtCore.Qt.AlignCenter, mode, state) + return pm + + def paint(self, painter, rect, alignment, mode, state): + if self.parent: + # Set color from parent + painter.setPen(self.parent.palette().text().color()) + self.svg.render(painter, rect) + + +class SvgIcon(QIcon): + '''A QIcon that renders a styled svg. + + Arguments: + svg_file: Path to svg file + parent: Parent widget used for coloring + ''' + + def __init__(self, svg_file, parent=None): + super(SvgIcon, self).__init__(SvgIconEngine(svg_file, parent)) + self.svg_file = svg_file + + +class FontIconEngine(QIconEngine): + '''Handles painting of Font Icons.''' + + def __init__(self, char, family, parent=None): + super(FontIconEngine, self).__init__() + self.char = char + self.family = family + self.parent = parent + + def pixmap(self, size, mode, state): + img = QImage(size, QImage.Format_ARGB32) + img.fill(QColor(0, 0, 0, 0)) + pm = QPixmap.fromImage(img) + painter = QPainter(pm) + self.paint(painter, pm.rect(), QtCore.Qt.AlignCenter, mode, state) + return pm + + def paint(self, painter, rect, alignment, mode, state): + font = painter.font() + font.setPixelSize(max(rect.width(), rect.height())) + font.setFamily(self.family) + painter.setFont(font) + if self.parent: + # Set color from parent + painter.setPen(self.parent.palette().text().color()) + painter.drawText(rect, alignment, self.char) + + +class FontIcon(QIcon): + '''A QIcon that renders a styled font icon glyph. + + Arguments: + family: Font family + char: Unicode character + parent: Parent widget used for coloring + ''' + + def __init__(self, char, family=None, parent=None): + family = family or 'construct' + super(FontIcon, self).__init__(FontIconEngine(char, family, parent)) + self.family = family + self.char = char diff --git a/construct/ui/layouts.py b/construct/ui/layouts.py new file mode 100644 index 0000000..841ad89 --- /dev/null +++ b/construct/ui/layouts.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Third party imports +from Qt import QtCore +from Qt.QtWidgets import QBoxLayout, QLayout, QSizePolicy, QStyle + +# Local imports +from .scale import pt + + +class BarLayout(QBoxLayout): + '''BarLayout like BoxLayout but with 3 sub layouts allowing users to add + widgets to the start, middle and end of the layout. + + |start | -------- middle -------- | end| + + Attributes: + start - QBoxLayout with default direction set to LeftToRight + middle - QBoxLayout with default direction set to LeftToRight + end - QBoxLayout with default direction set to LeftToRight + ''' + + def __init__(self, direction=None, parent=None): + direction = direction or self.LeftToRight + super(BarLayout, self).__init__(direction, parent) + + self.start = QBoxLayout(direction) + self.start.setSpacing(pt(8)) + self.left = self.start + self.top = self.start + + self.middle = QBoxLayout(direction) + self.middle.setSpacing(pt(8)) + self.center = self.middle + + self.end = QBoxLayout(direction) + self.end.setSpacing(pt(8)) + self.right = self.end + self.bottom = self.end + + self.addLayout(self.start) + self.addLayout(self.middle) + self.addLayout(self.end) + + self.setStretch(1, 1) + self.setSpacing(pt(8)) + self.setContentsMargins(0, 0, 0, 0) + + self.setDirection(direction) + + def count(self): + '''Sum of the count of all sub layouts''' + return self.start.count() + self.middle.count() + self.end.count() + + def setDirection(self, direction): + '''Realign sub-layouts based on direction''' + + if direction in (self.LeftToRight, self.RightToLeft): + self.start.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.middle.setAlignment(QtCore.Qt.AlignCenter) + self.end.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter) + elif direction in (self.TopToBottom, self.BottomToTop): + self.start.setAlignment(QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter) + self.middle.setAlignment(QtCore.Qt.AlignCenter) + self.end.setAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignHCenter) + else: + raise ValueError( + 'Got %s expected QBoxLayout.Direction' % direction + ) + + self.start.setDirection(direction) + self.middle.setDirection(direction) + self.end.setDirection(direction) + super(BarLayout, self).setDirection(direction) + + +class HBarLayout(BarLayout): + '''Horizontal BarLayout. + + Attributes: + left - QBoxLayout alignment set to left + center - QBoxLayout alignment set to center + right - QBoxLayout alignment set to right + ''' + + def __init__(self, parent=None): + super(HBarLayout, self).__init__(BarLayout.LeftToRight, parent) + + +class VBarLayout(BarLayout): + '''Vertical BarLayout. + + Attributes: + top - QBoxLayout alignment set to top + center - QBoxLayout alignment set to center + bottom - QBoxLayout alignment set to bottom + ''' + + def __init__(self, parent=None): + super(VBarLayout, self).__init__(BarLayout.TopToBottom, parent) + + +class FlowLayout(QLayout): + '''A Layout that wraps widgets on overflow. + ____________ + | [] [] [] | + | [] [] [] | + | [] [] | + ------------ + ''' + + def __init__(self, margin=16, hspacing=8, vspacing=8, parent=None): + super(FlowLayout, self).__init__(parent=parent) + self._margin = pt(margin) + self._hspacing = pt(hspacing) + self._vspacing = pt(vspacing) + self._items = [] + self.setContentsMargins(margin, margin, margin, margin) + + def __del__(self): + while self.count(): + item = self.takeAt(0) + del item + + def addItem(self, item): + self._items.append(item) + + def count(self): + return len(self._items) + + def itemAt(self, index): + try: + return self._items[index] + except IndexError: + return + + def takeAt(self, index): + return self._items.pop(index) + + def horizontalSpacing(self): + return self._hspacing + + def verticalSpacing(self): + return self._vspacing + + def expandingDirections(self): + return QtCore.Qt.Horizontal + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self.doLayout( + QtCore.QRect(0, 0, width, 0), + True + ) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self.doLayout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize() + for item in self._items: + size.expandedTo(item.minimumSize()) + + margins = self.contentsMargins() + size += QtCore.QSize( + margins.left() + margins.right(), + margins.top() + margins.bottom() + ) + return size + + def doLayout(self, rect, testOnly=False): + + m = self.contentsMargins() + r = rect.adjusted(m.left(), m.top(), -m.right(), -m.bottom()) + x, y = r.x(), r.y() + rows = [] + row = {'items': [], 'height': 0, 'space': 0, 'count': 0} + rows.append(row) + + for item in self._items: + w = item.widget() + width, height = w.sizeHint().width(), w.sizeHint().height() + space = self.horizontalSpacing() + if space < 0: + space = w.style().layoutSpacing( + QSizePolicy.QPushButton, + QSizePolicy.QPushButton, + QtCore.Qt.Horizontal, + ) + + if x + width < r.right(): + row['items'].append(item) + row['height'] = max(row['height'], height) + row['space'] += space + row['count'] += 1 + x += width + space + else: + row['space'] += r.right() - x - space + row = { + 'items': [item], + 'height': height, + 'space': space, + 'count': 1 + } + rows.append(row) + x = r.x() + width + space + + if testOnly: + if len(rows) < 2: + vspace = 0 + else: + vspace = (len(rows) - 1) * self.verticalSpacing() + vmargins = m.top() + m.bottom() + return sum([r['height'] for r in rows]) + vspace + vmargins + + x, y = r.x(), r.y() + for row in rows: + + if row['count'] < 2: + space = 0 + else: + space = row['space'] / len(row['items']) - 1 + + for item in row['items']: + item.setGeometry(QtCore.QRect( + QtCore.QPoint(x, y), + item.sizeHint() + )) + x += item.sizeHint().width() + space + + x = r.x() + y += row['height'] + self.verticalSpacing() diff --git a/construct/ui/manager.py b/construct/ui/manager.py new file mode 100644 index 0000000..3930f12 --- /dev/null +++ b/construct/ui/manager.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + + +# Local imports +from .eventloop import requires_event_loop +from .resources import Resources +from .theme import theme + + +class UIManager(object): + '''Manages UI resources, themes, and dialogs.''' + + def __init__(self, api): + self.api = api + self.resources = Resources([]) + self.theme = theme + + def load(self): + self.resources = Resources(self.api.path) + self.theme.set_resources(self.resources) + self.api.extend('alert', self.alert) + self.api.extend('error', self.error) + self.api.extend('success', self.success) + self.api.extend('info', self.info) + self.api.extend('ask', self.ask) + + def unload(self): + self.resources = Resources([]) + self.theme.set_resources(self.resources) + self.api.unextend('alert') + self.api.unextend('error') + self.api.unextend('success') + self.api.unextend('ask') + + @requires_event_loop + def alert(self, message, title=None, short=None): + '''Create an alert dialog. + + Arguments: + message (str): Message to display + title (str): Optional title + ''' + from .dialogs import Notification + return Notification( + type='alert', + message=message, + title=title, + icon='alert', + close_icon='close', + short=short, + ).exec_() + + @requires_event_loop + def error(self, message, title=None, short=None): + '''Create an error dialog. + + Arguments: + message (str): Message to display + title (str): Optional title + ''' + from .dialogs import Notification + return Notification( + type='error', + message=message, + title=title, + icon='error', + close_icon='close', + short=short, + ).exec_() + + @requires_event_loop + def success(self, message, title=None, short=None): + '''Create a success dialog. + + Arguments: + message (str): Message to display + title (str): Optional title + ''' + from .dialogs import Notification + return Notification( + type='success', + message=message, + title=title, + close_icon='check', + short=short, + ).exec_() + + @requires_event_loop + def info(self, message, title=None, short=None): + '''Create an info dialog. + + Arguments: + message (str): Message to display + title (str): Optional title + ''' + from .dialogs import Notification + return Notification( + type='info', + message=message, + title=title, + close_icon='check', + short=short, + ).exec_() + + @requires_event_loop + def ask(self, title, message, yes_label='Yes', no_label='No', icon=None): + '''Ask a question. + + Arguments: + title (str): Title + message (str): Message to display + yes_label (str): Yes button text + no_label (str): No button text + icon (str): Icon to appear in header. + ''' + from .dialogs import Ask + return Ask( + title=title, + message=message, + yes_label=yes_label, + no_label=no_label, + icon=icon, + ).exec_() diff --git a/construct/ui/resources/__init__.py b/construct/ui/resources/__init__.py new file mode 100644 index 0000000..6fff1b5 --- /dev/null +++ b/construct/ui/resources/__init__.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import glob +import inspect +import json +import logging +import os +import re + +# Third party imports +from six import reraise + +# Local imports +from ...compat import Path, wraps +from ...utils import unipath + + +_log = logging.getLogger('construct.ui.resources') + +IMAGE_EXTENSIONS = [ + '.bmp', + '.gif', + '.jpg', + '.jpeg', + '.png', + '.pbm', + '.pgm', + '.ppm', + '.xbm', + '.xpm', + '.svg', +] +RESOURCE_EXTENSIONS = IMAGE_EXTENSIONS + [ + '.ttf', + '.scss', + '.css', +] +package_path = Path(__file__).parent.resolve() +missing = object() + + +class ResourceNotFoundError(Exception): + '''Raised when a resource can not be found.''' + + +class Resources(object): + '''Work with resources for an API object. + + Arguments: + lookup_paths (Path): A list of Path objects usually provided by an API + + Resource Resolution: + 1. Lookup resources relative to lookup_paths + 2. Lookup resources in BuiltinResources + ''' + + font_ids = {} + font_charmaps = {} + image_extensions = IMAGE_EXTENSIONS + resource_extensions = RESOURCE_EXTENSIONS + + def __init__(self, lookup_paths=None): + self.lookup_paths = lookup_paths or [] + self.builtin_resources = BuiltinResources() + self.loaded = False + self.path = package_path + + def load(self): + if self.loaded: + return + + from Qt.QtGui import QFontDatabase + + # Load fonts and charmaps + for resource, font in self.find(extensions=['.ttf']): + + if font.name in self.font_ids: + continue + + font_id = QFontDatabase.addApplicationFont(font.as_posix()) + font_families = QFontDatabase.applicationFontFamilies(font_id) + self.font_ids[font.name] = font_id + + # Look for charmaps + charmap = Path(str(font).replace('.ttf', '_charmap.json')) + if not charmap.is_file(): + continue + + charmap_data = json.loads(charmap.read_text()) + for family in font_families: + self.font_charmaps[family] = charmap_data + + self.loaded = True + + def get(self, resource, default=missing): + '''get a resource by relative path.''' + + for path in self.lookup_paths: + potential_path = path / resource + if potential_path.exists(): + return potential_path + + return self.builtin_resources.get(resource, default) + + def get_char(self, char, family=None): + family = family or 'construct' + try: + return self.font_charmaps[family][char] + except KeyError: + raise ResourceNotFoundError( + 'Could not find char: %s in font family %s' % (char, family) + ) + + def find(self, search=None, extensions=None): + '''List all resources. + + Arguments: + search (str): String to match in resource names + extensions (List[str]): List of extensions to match + + Returns: + List of tuples - (resource, resource_file) + ''' + + # Get resources on api.path + extensions = extensions or RESOURCE_EXTENSIONS + resources = {} + for path in self.lookup_paths: + for ext in extensions: + for file in path.glob('*/*' + ext): + resource = file.relative_to(path).as_posix().lstrip('./') + if search and search not in resource: + continue + resources.setdefault(resource, file) + + # Combine with BuiltinResources and sort + bresources = self.builtin_resources.find(search, extensions) + return sorted(list(resources.items()) + bresources) + + +class BuiltinResources(object): + '''Work with builtin resources including icons, branding, stylesheets + and fonts. + ''' + + _resources = {} + + def __init__(self): + self._update_resources() + + def __contains__(self, resource): + return resource in self._resources + + def _update_resources(self): + '''Update BuiltinResources._resources dict.''' + + if self._resources: + return + + for file in package_path.glob('*/*.*'): + + if file.suffix not in RESOURCE_EXTENSIONS: + continue + + rel_file = file.relative_to(package_path).as_posix().lstrip('./') + self._resources[rel_file] = file + + def get(self, resource, default=missing): + '''Get a resource's filepath''' + + if resource not in self._resources: + if default is not missing: + return default + raise ResourceNotFoundError('Could not find resource: ' + resource) + + return self._resources[resource] + + def find(self, search=None, extensions=None): + '''List all resources. + + Arguments: + search (str): String to match in resource names + extensions (List[str]): List of extensions to match + + Returns: + List of tuples - (resource, resource_file) + ''' + + resources = [] + for resource, resource_file in self._resources.items(): + if search and search not in resource: + continue + if extensions and resource_file.suffix not in extensions: + continue + resources.append((resource, resource_file)) + return sorted(resources) diff --git a/construct/ui/resources/brand/construct_icon-black.png b/construct/ui/resources/brand/construct_icon-black.png new file mode 100644 index 0000000..847904d Binary files /dev/null and b/construct/ui/resources/brand/construct_icon-black.png differ diff --git a/construct/ui/resources/brand/construct_icon-white-on-black.png b/construct/ui/resources/brand/construct_icon-white-on-black.png new file mode 100644 index 0000000..10d00ab Binary files /dev/null and b/construct/ui/resources/brand/construct_icon-white-on-black.png differ diff --git a/construct/ui/resources/brand/construct_icon-white.png b/construct/ui/resources/brand/construct_icon-white.png new file mode 100644 index 0000000..bca2256 Binary files /dev/null and b/construct/ui/resources/brand/construct_icon-white.png differ diff --git a/construct/ui/resources/fonts/LICENSE.txt b/construct/ui/resources/fonts/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/construct/ui/resources/fonts/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/construct/ui/resources/fonts/Roboto-Black.ttf b/construct/ui/resources/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..689fe5c Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Black.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-BlackItalic.ttf b/construct/ui/resources/fonts/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..0b4e0ee Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-BlackItalic.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-Bold.ttf b/construct/ui/resources/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..d3f01ad Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Bold.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-BoldItalic.ttf b/construct/ui/resources/fonts/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..41cc1e7 Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-BoldItalic.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-Italic.ttf b/construct/ui/resources/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..6a1cee5 Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Italic.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-Light.ttf b/construct/ui/resources/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..219063a Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Light.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-LightItalic.ttf b/construct/ui/resources/fonts/Roboto-LightItalic.ttf new file mode 100644 index 0000000..0e81e87 Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-LightItalic.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-Medium.ttf b/construct/ui/resources/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..1a7f3b0 Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Medium.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-MediumItalic.ttf b/construct/ui/resources/fonts/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..0030295 Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-MediumItalic.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-Regular.ttf b/construct/ui/resources/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2c97eea Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Regular.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-Thin.ttf b/construct/ui/resources/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..b74a4fd Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-Thin.ttf differ diff --git a/construct/ui/resources/fonts/Roboto-ThinItalic.ttf b/construct/ui/resources/fonts/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..dd0ddb8 Binary files /dev/null and b/construct/ui/resources/fonts/Roboto-ThinItalic.ttf differ diff --git a/construct/ui/resources/fonts/construct.ttf b/construct/ui/resources/fonts/construct.ttf new file mode 100644 index 0000000..81de8ab Binary files /dev/null and b/construct/ui/resources/fonts/construct.ttf differ diff --git a/construct/ui/resources/fonts/construct_charmap.json b/construct/ui/resources/fonts/construct_charmap.json new file mode 100644 index 0000000..c1a0192 --- /dev/null +++ b/construct/ui/resources/fonts/construct_charmap.json @@ -0,0 +1,46 @@ +{ + "alert": "\ue901", + "arrow_down": "\ue902", + "arrow_down_solid": "\ue903", + "arrow_left": "\ue904", + "arrow_left_solid": "\ue905", + "arrow_right": "\ue906", + "arrow_right_solid": "\ue907", + "arrow_up": "\ue908", + "arrow_up_solid": "\ue909", + "back": "\ue90a", + "bookmark": "\ue90b", + "bookmark_outline": "\ue90c", + "check": "\ue90d", + "checkbox_check": "\ue90e", + "checkbox_off": "\ue90f", + "checkbox_on": "\ue910", + "circle": "\ue911", + "circle_outline": "\ue912", + "close": "\ue913", + "collection": "\ue914", + "construct": "\ue915", + "error": "\ue916", + "filter": "\ue917", + "folder": "\ue918", + "folder_open": "\ue919", + "forward": "\ue91a", + "grid": "\ue91b", + "group_list": "\ue91c", + "home": "\ue91d", + "list": "\ue91e", + "menu": "\ue91f", + "menu_dots": "\ue920", + "minus": "\ue921", + "plus": "\ue922", + "question": "\ue923", + "refresh": "\ue924", + "search": "\ue925", + "settings": "\ue926", + "sort": "\ue927", + "square": "\ue928", + "square_outline": "\ue929", + "tree": "\ue92a", + "triangle": "\ue92b", + "triangle_outline": "\ue900" +} diff --git a/construct/ui/resources/icons/alert.svg b/construct/ui/resources/icons/alert.svg new file mode 100644 index 0000000..3d9ac03 --- /dev/null +++ b/construct/ui/resources/icons/alert.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_down.svg b/construct/ui/resources/icons/arrow_down.svg new file mode 100644 index 0000000..76f1373 --- /dev/null +++ b/construct/ui/resources/icons/arrow_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_down_solid.svg b/construct/ui/resources/icons/arrow_down_solid.svg new file mode 100644 index 0000000..6649c66 --- /dev/null +++ b/construct/ui/resources/icons/arrow_down_solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_left.svg b/construct/ui/resources/icons/arrow_left.svg new file mode 100644 index 0000000..3ebded7 --- /dev/null +++ b/construct/ui/resources/icons/arrow_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_left_solid.svg b/construct/ui/resources/icons/arrow_left_solid.svg new file mode 100644 index 0000000..9b536f6 --- /dev/null +++ b/construct/ui/resources/icons/arrow_left_solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_right.svg b/construct/ui/resources/icons/arrow_right.svg new file mode 100644 index 0000000..aafa040 --- /dev/null +++ b/construct/ui/resources/icons/arrow_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_right_solid.svg b/construct/ui/resources/icons/arrow_right_solid.svg new file mode 100644 index 0000000..1f6bc3f --- /dev/null +++ b/construct/ui/resources/icons/arrow_right_solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_up.svg b/construct/ui/resources/icons/arrow_up.svg new file mode 100644 index 0000000..c41feb8 --- /dev/null +++ b/construct/ui/resources/icons/arrow_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/arrow_up_solid.svg b/construct/ui/resources/icons/arrow_up_solid.svg new file mode 100644 index 0000000..fadb455 --- /dev/null +++ b/construct/ui/resources/icons/arrow_up_solid.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/back.svg b/construct/ui/resources/icons/back.svg new file mode 100644 index 0000000..3fde354 --- /dev/null +++ b/construct/ui/resources/icons/back.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/bookmark.svg b/construct/ui/resources/icons/bookmark.svg new file mode 100644 index 0000000..ffd5c5f --- /dev/null +++ b/construct/ui/resources/icons/bookmark.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/bookmark_outline.svg b/construct/ui/resources/icons/bookmark_outline.svg new file mode 100644 index 0000000..4df48c2 --- /dev/null +++ b/construct/ui/resources/icons/bookmark_outline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/construct/ui/resources/icons/check.svg b/construct/ui/resources/icons/check.svg new file mode 100644 index 0000000..01d8a6d --- /dev/null +++ b/construct/ui/resources/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/checkbox_check.svg b/construct/ui/resources/icons/checkbox_check.svg new file mode 100644 index 0000000..2576240 --- /dev/null +++ b/construct/ui/resources/icons/checkbox_check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/construct/ui/resources/icons/checkbox_off.svg b/construct/ui/resources/icons/checkbox_off.svg new file mode 100644 index 0000000..80e5df8 --- /dev/null +++ b/construct/ui/resources/icons/checkbox_off.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/checkbox_on.svg b/construct/ui/resources/icons/checkbox_on.svg new file mode 100644 index 0000000..6086516 --- /dev/null +++ b/construct/ui/resources/icons/checkbox_on.svg @@ -0,0 +1,4 @@ + + + + diff --git a/construct/ui/resources/icons/circle.svg b/construct/ui/resources/icons/circle.svg new file mode 100644 index 0000000..60df7b4 --- /dev/null +++ b/construct/ui/resources/icons/circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/circle_outline.svg b/construct/ui/resources/icons/circle_outline.svg new file mode 100644 index 0000000..1d6a780 --- /dev/null +++ b/construct/ui/resources/icons/circle_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/close.svg b/construct/ui/resources/icons/close.svg new file mode 100644 index 0000000..b3b7686 --- /dev/null +++ b/construct/ui/resources/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/collection.svg b/construct/ui/resources/icons/collection.svg new file mode 100644 index 0000000..de8fe2d --- /dev/null +++ b/construct/ui/resources/icons/collection.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/construct/ui/resources/icons/construct.svg b/construct/ui/resources/icons/construct.svg new file mode 100644 index 0000000..21c575f --- /dev/null +++ b/construct/ui/resources/icons/construct.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/construct/ui/resources/icons/error.svg b/construct/ui/resources/icons/error.svg new file mode 100644 index 0000000..d7b975d --- /dev/null +++ b/construct/ui/resources/icons/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/filter.svg b/construct/ui/resources/icons/filter.svg new file mode 100644 index 0000000..935a7c0 --- /dev/null +++ b/construct/ui/resources/icons/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/folder.svg b/construct/ui/resources/icons/folder.svg new file mode 100644 index 0000000..2ee6bbf --- /dev/null +++ b/construct/ui/resources/icons/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/folder_open.svg b/construct/ui/resources/icons/folder_open.svg new file mode 100644 index 0000000..3873893 --- /dev/null +++ b/construct/ui/resources/icons/folder_open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/construct/ui/resources/icons/forward.svg b/construct/ui/resources/icons/forward.svg new file mode 100644 index 0000000..5795e9e --- /dev/null +++ b/construct/ui/resources/icons/forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/grid.svg b/construct/ui/resources/icons/grid.svg new file mode 100644 index 0000000..911a587 --- /dev/null +++ b/construct/ui/resources/icons/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/construct/ui/resources/icons/group_list.svg b/construct/ui/resources/icons/group_list.svg new file mode 100644 index 0000000..6bf1426 --- /dev/null +++ b/construct/ui/resources/icons/group_list.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/construct/ui/resources/icons/home.svg b/construct/ui/resources/icons/home.svg new file mode 100644 index 0000000..7ae1a77 --- /dev/null +++ b/construct/ui/resources/icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/list.svg b/construct/ui/resources/icons/list.svg new file mode 100644 index 0000000..0fcbecd --- /dev/null +++ b/construct/ui/resources/icons/list.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/construct/ui/resources/icons/menu.svg b/construct/ui/resources/icons/menu.svg new file mode 100644 index 0000000..da0a8a2 --- /dev/null +++ b/construct/ui/resources/icons/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/construct/ui/resources/icons/menu_dots.svg b/construct/ui/resources/icons/menu_dots.svg new file mode 100644 index 0000000..ac64275 --- /dev/null +++ b/construct/ui/resources/icons/menu_dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/construct/ui/resources/icons/minus.svg b/construct/ui/resources/icons/minus.svg new file mode 100644 index 0000000..4ebeec0 --- /dev/null +++ b/construct/ui/resources/icons/minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/plus.svg b/construct/ui/resources/icons/plus.svg new file mode 100644 index 0000000..0811c01 --- /dev/null +++ b/construct/ui/resources/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/question.svg b/construct/ui/resources/icons/question.svg new file mode 100644 index 0000000..32232f6 --- /dev/null +++ b/construct/ui/resources/icons/question.svg @@ -0,0 +1,4 @@ + + + + diff --git a/construct/ui/resources/icons/refresh.svg b/construct/ui/resources/icons/refresh.svg new file mode 100644 index 0000000..5c65043 --- /dev/null +++ b/construct/ui/resources/icons/refresh.svg @@ -0,0 +1,4 @@ + + + + diff --git a/construct/ui/resources/icons/search.svg b/construct/ui/resources/icons/search.svg new file mode 100644 index 0000000..85019af --- /dev/null +++ b/construct/ui/resources/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/settings.svg b/construct/ui/resources/icons/settings.svg new file mode 100644 index 0000000..215c6e4 --- /dev/null +++ b/construct/ui/resources/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/sort.svg b/construct/ui/resources/icons/sort.svg new file mode 100644 index 0000000..1e9fb7d --- /dev/null +++ b/construct/ui/resources/icons/sort.svg @@ -0,0 +1,4 @@ + + + + diff --git a/construct/ui/resources/icons/square.svg b/construct/ui/resources/icons/square.svg new file mode 100644 index 0000000..eca7f44 --- /dev/null +++ b/construct/ui/resources/icons/square.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/square_outline.svg b/construct/ui/resources/icons/square_outline.svg new file mode 100644 index 0000000..80e5df8 --- /dev/null +++ b/construct/ui/resources/icons/square_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/tree.svg b/construct/ui/resources/icons/tree.svg new file mode 100644 index 0000000..0d4eb68 --- /dev/null +++ b/construct/ui/resources/icons/tree.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/triangle.svg b/construct/ui/resources/icons/triangle.svg new file mode 100644 index 0000000..e8bc4f2 --- /dev/null +++ b/construct/ui/resources/icons/triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/icons/triangle_outline.svg b/construct/ui/resources/icons/triangle_outline.svg new file mode 100644 index 0000000..d73275d --- /dev/null +++ b/construct/ui/resources/icons/triangle_outline.svg @@ -0,0 +1,3 @@ + + + diff --git a/construct/ui/resources/styles/_funcs.scss b/construct/ui/resources/styles/_funcs.scss new file mode 100644 index 0000000..dd66aef --- /dev/null +++ b/construct/ui/resources/styles/_funcs.scss @@ -0,0 +1,37 @@ +@function luma($color) { + $r: red($color) / 255; + $g: green($color) / 255; + $b: blue($color) / 255; + @return $r * 0.3 + $g * 0.59 + $b * 0.11; +} + +@function contrast_ratio($c1, $c2) { + @return (luma($c1) + 0.05) / (luma($c2) + 0.05); +} + + +@function brightness($color) { + @return ((red($color) + green($color) + blue($color)) / 3) / 255; +} + + +@function vary($color, $percent) { + @if luma($color) < 0.25 { + @return lighten($color, $percent); + } @else { + @return darken($color, $percent); + } +} + + +@function vary_on($on_color, $color, $percent) { + + $l1: luma($on_color); + $l2: luma($color); + + @if $l1 < $l2 { + @return darken($on_color, $percent); + } @else { + @return lighten($on_color, $percent); + } +} diff --git a/construct/ui/resources/styles/_icons.scss b/construct/ui/resources/styles/_icons.scss new file mode 100644 index 0000000..9f91792 --- /dev/null +++ b/construct/ui/resources/styles/_icons.scss @@ -0,0 +1,46 @@ +$icomoon-font-family: "construct" !default; +$icomoon-font-path: "fonts" !default; + +$icon-alert: "\e900"; +$icon-arrow_down: "\e901"; +$icon-arrow_down_solid: "\e902"; +$icon-arrow_left: "\e903"; +$icon-arrow_left_solid: "\e904"; +$icon-arrow_right: "\e905"; +$icon-arrow_right_solid: "\e906"; +$icon-arrow_up: "\e907"; +$icon-arrow_up_solid: "\e908"; +$icon-back: "\e909"; +$icon-bookmark: "\e90a"; +$icon-check: "\e90b"; +$icon-checkbox_check: "\e90c"; +$icon-checkbox_off: "\e90d"; +$icon-checkbox_on: "\e90e"; +$icon-circle: "\e90f"; +$icon-circle_outline: "\e910"; +$icon-close: "\e911"; +$icon-collection: "\e912"; +$icon-error: "\e913"; +$icon-filter: "\e914"; +$icon-folder: "\e915"; +$icon-folder_open: "\e916"; +$icon-forward: "\e917"; +$icon-grid: "\e918"; +$icon-group_list: "\e919"; +$icon-home: "\e91a"; +$icon-list: "\e91b"; +$icon-menu: "\e91c"; +$icon-menu_dots: "\e91d"; +$icon-minus: "\e91e"; +$icon-plus: "\e91f"; +$icon-question: "\e920"; +$icon-refresh: "\e921"; +$icon-search: "\e922"; +$icon-settings: "\e923"; +$icon-sort: "\e924"; +$icon-square: "\e925"; +$icon-square_outline: "\e926"; +$icon-tree: "\e927"; +$icon-triangle: "\e928"; +$icon-triangle_outline: "\e929"; + diff --git a/construct/ui/resources/styles/theme.scss b/construct/ui/resources/styles/theme.scss new file mode 100644 index 0000000..b6790ec --- /dev/null +++ b/construct/ui/resources/styles/theme.scss @@ -0,0 +1,331 @@ +// Fonts +$font_stack: Roboto !default; +$font_size: 12pt !default; +$font_weight: 400 !default; + +// Colors +$primary: #FF5862 !default; +$primary_variant: #D72F57 !default; +$on_primary: #FFFFFF !default; +$alert: #FACE49 !default; +$error: #F15555 !default; +$success: #7EDC9E !default; +$info: #87CDEB !default; +$on_secondary: #000000 !default; +$background: #33333D !default; +$on_background: #F1F1F3 !default; +$surface: #4F4F59 !default; +$on_surface: #F1F1F3 !default; +$transparent: rgba(0, 0, 0, 0); + + +@import 'funcs'; + + +// Mixins // + + +@mixin roboto { + font-family: $font_stack; + font-weight: $font_weight; + font-size: $font_size; +} + +@mixin roboto-light { + font-family: $font_stack; + font-weight: $font_weight - 100; + font-size: $font_size; +} + +@mixin roboto-medium { + font-family: $font_stack; + font-weight: $font_weight + 100; + font-size: $font_size; +} + +@mixin icon { + margin: 0; + padding: 0; + list-style: none; + font-family: "construct" !important; + font-size: $font_size; +} + + +// Base Widgets // + + +QWidget { + @include roboto; +} + +*[status="alert"] { + color: $alert; + border-color: $alert; +} + +*[status="error"] { + color: $error; + border-color: $error; +} + +*[status="success"] { + color: $success; + border-color: $success; +} + +*[status="info"] { + color: $info; + border-color: $info; +} + +#icon { + @include icon; + background-color: $transparent; +} + +#h1 { + @include roboto; + font-size: 24pt; +} + +#h2 { + @include roboto-medium; + font-size: 20pt; +} + +#h3 { + @include roboto; + font-size: 18pt; +} + +#h4 { + @include roboto; + font-size: 14pt; +} + +#h5 { + @include roboto-medium; + font-size: 14pt; +} + +#p { + @include roboto; + font-size: 12pt; +} + + +// Base theme +@mixin base( + $primary: #CC4455, + $on_primary: #000000, + $bg: #FFFFFF, + $on_bg: #000000 +) { + + background: $bg; + + * { + color: $on_bg; + } + + #icon { + outline: none; + border: 2 solid vary($bg, 5%); + border-radius: 3; + } + #icon:focus { + border: 2 solid vary($bg, 15%); + } + #icon:disabled { + border: 0; + } + #icon:hover { + background-color: vary($bg, 5%); + } + #icon:pressed { + background-color: vary($bg, 15%); + } + + QPushButton#text-button { + padding: scale_pt(8 24 8 24); + border: 2 solid vary($bg, 10%); + border-radius: 3; + } + QPushButton#text-button:disabled { + color: vary($bg, 15%); + } + QPushButton#text-button:hover { + color: $on_primary; + background-color: fade-out($primary, 0.55); + border: 2 solid fade-out($primary, 0.55); + } + QPushButton#text-button:focus { + outline: none; + border: 2 solid fade-out($primary, 0.25); + } + QPushButton#text-button:pressed { + color: $on_primary; + background-color: $primary; + } + QPushButton#tool-button { + padding: scale_pt(8 24 8 24); + border-radius: 3; + font-size: 8pt; + } + QPushButton#tool-button:disabled { + color: vary($bg, 15%); + } + QPushButton#tool-button:hover { + color: $on_primary; + background-color: fade-out($primary, 0.55); + border: 2 solid fade-out($primary, 0.55); + } + QPushButton#tool-button:focus { + outline: none; + border: 2 solid fade-out($primary, 0.25); + } + QPushButton#tool-button:pressed { + color: $on_primary; + background-color: $primary; + } +} + + +// Primary colored sub-theme // + + +QWidget#primary { + @include base( + $primary_variant, + $on_primary, + $primary, + $on_primary, + ); +} + +QDialog#primary { + border: 2 solid vary($primary, 20%); +} + + +// Surface colored sub-theme // + + +QWidget#surface { + @include base( + $primary, + $on_primary, + $surface, + $on_surface, + ); +} + +QDialog#surface { + border: 2 solid vary($surface, 10%); +} + +QDialog#surface:active { + border: 2 solid vary($surface, 20%); +} + +// Background colored sub-theme // + + +QWidget#background { + @include base( + $primary, + $on_primary, + $background, + $on_background, + ); +} + +QDialog#background { + border: 2 solid vary($background, 10%); +} + +QDialog#background:active { + border: 2 solid vary($background, 20%); +} + +// Alert colored sub-theme // + + +QWidget#alert { + @include base( + $alert, + $on_secondary, + $alert, + $on_secondary, + ); +} + +QDialog#alert { + border: 2 solid vary($alert, 10%); +} + +QDialog#alert:active { + border: 2 solid vary($alert, 20%); +} + +// Error colored sub-theme // + + +QWidget#error { + @include base( + $on_secondary, + $error, + $error, + $on_secondary, + ); +} + +QDialog#error { + border: 2 solid vary($error, 10%); +} + +QDialog#error:active { + border: 2 solid vary($error, 20%); +} + +// Success colored sub-theme // + + +QWidget#success { + @include base( + $on_secondary, + $success, + $success, + $on_secondary, + ); +} + +QDialog#success { + border: 2 solid vary($success, 10%); +} + +QDialog#success:active { + border: 2 solid vary($success, 20%); +} + + +// Info colored sub-theme // + + +QWidget#info { + @include base( + $on_secondary, + $info, + $info, + $on_secondary, + ); +} + +QDialog#info { + border: 2 solid vary($info, 10%); +} + +QDialog#info:active { + border: 2 solid vary($info, 20%); +} diff --git a/construct/ui/scale.py b/construct/ui/scale.py new file mode 100644 index 0000000..972ea26 --- /dev/null +++ b/construct/ui/scale.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + + +def dpi(): + '''Get screen DPI to scale UI independent of monitor size.''' + + from Qt import QtWidgets + + app = QtWidgets.QApplication.instance() + if app: + return float(app.desktop().logicalDpiX()) + return 96.0 + + +def factor(): + '''Get UI scale factor''' + + return dpi() / 96.0 + + +def px(value): + '''Scale a pixel value based on screen dpi.''' + + return int(factor() * value) + + +def pt(value): + '''Scale a point value based on screen dpi.''' + + return int(factor() * value * 1.33) diff --git a/construct/ui/theme.py b/construct/ui/theme.py new file mode 100644 index 0000000..9d2b3d2 --- /dev/null +++ b/construct/ui/theme.py @@ -0,0 +1,427 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Standard library imports +import logging +from functools import wraps + +# Third party imports +import qtsass + +# Local imports +from ..compat import basestring +from ..types import WeakSet +from . import resources, scale + + +_log = logging.getLogger(__name__) + + +def ensure_loaded(method): + '''A decorator that ensures an objects resources are loaded before + executing. + ''' + + @wraps(method) + def call_method(self, *args, **kwargs): + self.load() + resources = getattr(self, 'resources', None) + if resources: + resources.load() + return method(self, *args, **kwargs) + return call_method + + +def clamp(value, lower, upper): + '''Clamp a value between lower and upper bounds + + Arguments: + value (int or float): the number to clamp + lower (int or float): the lower bound + upper (int or float): the upper bound + ''' + return max(lower, min(value, upper)) + + +def rgb_to_hex(r, g, b, a=None): + '''Convert r, g, b ints to a hex string. + + Arguments: + r (int): Red value between 0 and 255 + g (int): Green value between 0 and 255 + b (int): Blue value between 0 and 255 + a (int): OPTIONAL - Alpha value between 0 and 255 + Returns: + Hex string in "#RRGGBB" or "#RRGGBBAA" format + ''' + + code = '#{:02x}{:02x}{:02x}'.format( + clamp(r, 0, 255), + clamp(g, 0, 255), + clamp(b, 0, 255), + ) + if a: + code += '{:02x}'.format(clamp(a, 0, 255)) + return code + + +def is_rgb(value): + '''Return True if value is an rgb or rgba sequence.''' + + return isinstance(value, (tuple, list)) and len(value) in (3, 4) + + +def hex_to_rgb(code): + '''Convert a hex code to rgb tuple. + + Arguments: + code (str): Hex code string like #FFFFFF + Returns: + Tuple of ints - (r, g, b) or (r, g, b, a) + ''' + + return tuple([ord(i) for i in code[1:].decode('hex')]) + + +def is_hex_code(value): + '''Return True if value is a hex code''' + + if not isinstance(value, basestring): + return False + if value.startswith('#'): + return True + + +class Theme(object): + """Represents the UI Font and Color theme. + + Colors are based on the Material design color system. All attributes can + be passed to the constructor. Colors can be passed as either Hex strings + or RGB tuples. + + Attributes: + name (str): name of the theme + resources (Resource): resource object + stylesheet (str): The compiled stylesheet + + Font Attributes: + font_stack (str): Comma separated list of font-families + font_size (str): Font point size + font_weight (str): Font weight + + Color Attributes: + primary (color): Used for accents and emphasis + primary_variant (color): Variant of primary color + on_primary (color): Color of type and icons on primary + alert (color): Indicates warnings and alerts + error (color): Indicates error + success (color): Indicates success + info (color): Indicates info + on_secondary (color): Color of type and icons on secondary colors + background (color): Used for underlying background fill + on_background (color): Color of type and icons on background + surface (color): Background color of most cards and widgets + on_surface (color): Color of type on surface + + Arguments: + resources (Resource): Used to lookup icons, pixmaps, and images + **options: Pass any of the font or color attributes listed above + """ + + defaults = dict( + name='default', + font_stack='Roboto', + font_size='12pt', + font_weight='400', + primary='#FF5862', + primary_variant='#D72F57', + on_primary='#FFFFFF', + alert='#FACE49', + error='#F15555', + success='#7EDC9E', + info='#87CDEB', + on_secondary='#000000', + background='#33333D', + on_background='#F1F1F3', + surface='#4F4F59', + on_surface='#F1F1F3', + ) + color_variables = [ + 'primary', + 'primary_variant', + 'on_primary', + 'alert', + 'error', + 'success', + 'info', + 'on_secondary', + 'background', + 'on_background', + 'surface', + 'on_surface', + ] + variables = list(defaults.keys()) + + def __init__(self, resources, **options): + self.__dict__.update(**self.defaults) + self.__dict__.update(**options) + self.resources = resources + self._stylesheet = None + self._widgets = WeakSet() + self._signals = None + self._loaded = False + + def load(self): + '''Creates a QObject used to dispatch on_theme_changed callbacks.''' + + if self._loaded: + return + + from Qt.QtCore import QObject, Signal + + class ThemeSignals(QObject): + changed = Signal(str) + + self._signals = ThemeSignals() + self._signals.changed.connect(self.on_theme_changed) + + self._loaded = True + + def set_resources(self, resources): + '''Sets the resources this theme uses''' + + self.resources = resources + + @ensure_loaded + def apply(self, widget): + '''Applies this theme to a widget and adds the widget to an internal + set. If the theme changes, the theme will be reapplied. + ''' + + widget.setStyleSheet(self.stylesheet) + self._widgets.add(widget) + + @property + def stylesheet(self): + if not self._stylesheet: + self._stylesheet = self.compile_stylesheet() + return self._stylesheet + + def compile_stylesheet(self): + '''Compile a stylesheet for this theme.''' + + sass = '' + var_tmpl = '${}: {};\n' + rgb_tmpl = '${}: rgb({}, {}, {});\n' + rgba_tmpl = '${}: rgba({}, {}, {}, {});\n' + for var in self.variables: + value = getattr(self, var) + if isinstance(value, basestring): + sass += var_tmpl.format(var, value) + elif isinstance(value, (tuple, list)): + if len(value) == 3: + sass += rgb_tmpl.format(var, *value) + elif len(value) == 4: + sass += rgba_tmpl.format(var, *value) + else: + raise ValueError( + '%s: Expected a length or 3 or 4 got %s.' % + (var, len(value)) + ) + else: + raise TypeError( + '%s: Expected (str, tuple) got %s' % + (var, type(value)) + ) + + sass += "@import 'theme'" + + css = qtsass.compile( + sass, + include_paths=[ + str(self.resources.path / 'styles'), + ], + custom_functions={ + 'res_url': sass_res_url(self), + 'scale_pt': sass_scale_pt(self), + 'scale_px': sass_scale_px(self), + }, + ) + return css + + def refresh_stylesheet(self): + '''Recompile stylesheet and reapply to all themed widgets.''' + self._stylesheet = self.compile_stylesheet() + if self._signals: + self._signals.changed.emit(self.stylesheet) + else: + self.on_theme_changed() + + def on_theme_changed(self): + '''Reapplies stylesheet to all themed widgets.''' + + for widget in self._widgets: + + # Reapply to registered widget + widget.setStyleSheet(self.stylesheet) + + # Reapply Stylesheet to children + children = widget.children() + while children: + child = children.pop() + if hasattr(child, 'setStyleSheet'): + child.setStyleSheet(self.stylesheet) + if hasattr(child, 'children'): + children.extend(child.children()) + + def set_color(self, name, value): + '''Set a color value for this theme. + + Colors are converted to hex codes here. + + Arguments: + name (str): Name of color attribute to set + value (tuple or str): rgb tuple or hex code + ''' + if name not in self.color_variables: + raise NameError('"%s" is not a valid color.' % name) + + if is_rgb(value): + setattr(self, name, rgb_to_hex(*value)) + elif is_hex_code(value): + setattr(self, name, value) + else: + raise ValueError('Expected rgb tuple or hex code got %s' % value) + + def hex(self, name): + '''Return the named color as a hex code string.''' + + if name not in self.color_variables: + raise NameError('"%s" is not a valid color.' % name) + + return getattr(self, name) + + def rgb(self, name): + '''Return the named color as an rgb tuple.''' + + if name not in self.color_variables: + raise NameError('"%s" is not a valid color.' % name) + + return hex_to_rgb(getattr(self, name)) + + @ensure_loaded + def pixmap(self, resource, size=None, family=None): + '''Get a pixmap for a resource. + + Arguments: + resource (str): Path relative to Construct PATH or font icon name + size (QSize): Size of pixmap to return + family (str): Font family for font icon character (optional) + ''' + + from Qt.QtGui import QPixmap, QIcon + from .icons import FontIcon + + path = self.resources.get(resource, None) + if path: + if path.suffix == '.svg': + return QIcon(str(path)).pixmap(size) + elif path.suffix in self.resources.image_extensions: + return QPixmap(str(path), size) + else: + raise TypeError("Can't create a pixmap for %s" % str(path)) + + char = self.resources.get_char(resource, family) + return FontIcon(char, family).pixmap(size) + + @ensure_loaded + def icon(self, resource, family=None, parent=None): + '''Get a QIcon for a resource. + + Arguments: + resource (str): Path relative to Construct PATH or font icon name + family (str): Font family for font icon character (optional) + parent (QWidget): Parent QWidget of QIcon - used for coloring + ''' + + from Qt.QtGui import QIcon + from .icons import SvgIcon, FontIcon + + path = self.resources.get(resource, None) + if path: + if path.suffix == '.svg': + return SvgIcon(str(path), parent) + elif path.suffix in self.resources.image_extensions: + return QIcon(str(path)) + else: + raise TypeError("Can't create a pixmap for %s" % str(path)) + + char = self.resources.get_char(resource, family) + return FontIcon(char, family, parent) + + +# Sass functions + +def sass_res_url(theme): + '''Get an url for a construct resource. + + Usage: + QPushButton {qproperty-icon: res_url(icons/plus.svg);} + ''' + def res_url(resource): + return 'url("%s")' % theme.resources.get(resource).as_posix() + return res_url + + +def sass_scale_pt(theme): + '''Convert pt value to dpi aware pixel value. + + Usage: + QPushButton {padding: scale_pt(8 24 8 24)} + QLabel {margin-left: scale_pt(8); margin-right: scale_pt(8);} + ''' + def scale_pt(value): + if isinstance(value, float): + return str(scale.pt(value)) + + # Handle sass types + import sass + if isinstance(value, sass.SassNumber): + return str(scale.pt(value.value)) + + if isinstance(value, sass.SassList): + result = [] + for item in value.items: + result.append(str(scale.pt(item.value))) + return ' '.join(result) + return scale_pt + + +def sass_scale_px(theme): + '''Convert pixel value to dpi aware pixel value. + + Usage: + QPushButton {padding: scale_px(8 24 8 24)} + QLabel {margin-left: scale_px(8); margin-right: scale_px(8);} + ''' + + def scale_px(value): + if isinstance(value, float): + return str(scale.px(value)) + + # Handle sass types + import sass + if isinstance(value, sass.SassNumber): + return str(scale.px(value.value)) + + if isinstance(value, sass.SassList): + result = [] + for item in value.items: + result.append(str(scale.px(item.value))) + return ' '.join(result) + return scale_px + + +# Theme singleton +theme = Theme(resources.Resources([])) diff --git a/construct/ui/widgets.py b/construct/ui/widgets.py new file mode 100644 index 0000000..1bc9eee --- /dev/null +++ b/construct/ui/widgets.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +# Third party imports +from Qt import QtCore, QtWidgets + +# Local imports +from .scale import pt +from .theme import theme + + +__all__ = [ + 'H1', + 'H2', + 'H3', + 'H4', + 'H5', + 'Header', + 'P', +] + + +class BaseLabel(QtWidgets.QLabel): + + css_id = '' + + def __init__(self, *args, **kwargs): + super(BaseLabel, self).__init__(*args, **kwargs) + self.setObjectName(self.css_id) + + +class H1(BaseLabel): + + css_id = 'h1' + + +class H2(BaseLabel): + + css_id = 'h2' + + +class H3(BaseLabel): + + css_id = 'h3' + + +class H4(BaseLabel): + + css_id = 'h2' + + +class H5(BaseLabel): + + css_id = 'h3' + + +class P(BaseLabel): + + css_id = 'p' + + def __init__(self, *args, **kwargs): + super(P, self).__init__(*args, **kwargs) + self.setWordWrap(True) + + +class Button(QtWidgets.QPushButton): + + css_id = 'text-button' + + def __init__(self, text, icon=None, icon_size=None, **kwargs): + super(Button, self).__init__(**kwargs) + self.setObjectName(self.css_id) + self.setText(text) + if icon: + self.setIcon(theme.icon(icon)) + if icon_size: + self.setIconSize(QtCore.QSize(pt(icon_size[0]), pt(icon_size[1]))) + + +class ToolButton(QtWidgets.QPushButton): + + css_id = 'tool-button' + + def __init__(self, text, icon=None, icon_size=None, **kwargs): + super(ToolButton, self).__init__(**kwargs) + self.setObjectName(self.css_id) + + self.label = P(text, parent=self) + self.label.setWordWrap(True) + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setMouseTracking(False) + self.label.setTextInteractionFlags(QtCore.Qt.NoTextInteraction) + self.label.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding, + ) + + self.glyph = Glyph(icon, icon_size, parent=self) + + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(pt(8), pt(8), pt(8), pt(8)) + self.layout.setSpacing(pt(4)) + self.layout.addWidget(self.glyph) + self.layout.addWidget(self.label) + self.setLayout(self.layout) + + def sizeHint(self): + return QtCore.QSize( + self.glyph.size.width() + pt(16), + self.glyph.size.height() + pt(32), + ) + + +class Glyph(QtWidgets.QLabel): + + css_id = 'icon' + + def __init__(self, icon, icon_size, parent=None): + super(Glyph, self).__init__(parent=parent) + + self.icon = theme.icon(icon, parent=parent) + if icon_size: + self.size = QtCore.QSize(pt(icon_size[0]), pt(icon_size[1])) + else: + self.size = QtCore.QSize(pt(24), pt(24)) + self.setPixmap(self.icon.pixmap(self.size)) + self.setFixedSize(self.size) + self.setSizePolicy( + QtWidgets.QSizePolicy.Fixed, + QtWidgets.QSizePolicy.Fixed, + ) diff --git a/construct/utils.py b/construct/utils.py index 778d374..a5e0805 100644 --- a/construct/utils.py +++ b/construct/utils.py @@ -1,157 +1,132 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import -__all__ = [ - 'ensure_callable', - 'ensure_instance', - 'ensure_type', - 'env_with_default', - 'get_qualname', - 'get_callable_name', - 'path_split', - 'pop_attr', - 'update_dict', - 'unipath', - 'package_path', - 'isolated_imports', - 'import_file', - 'iter_modules', - 'missing', - 'classproperty', - 'dummy_ctxmanager', - 'cached_property' -] +from __future__ import absolute_import -import inspect +# Standard library imports import os +import shutil import sys -from collections import Mapping -from fstrings import f -from glob import glob from contextlib import contextmanager -from construct.compat import basestring - - -platform = sys.platform.rstrip('0123456789').lower() -if platform == 'darwin': - platform = 'mac' -missing = object() - - -def package_path(*paths): - '''Return a path relative to the construct package''' - - return unipath(os.path.dirname(__file__), *paths) +# Third party imports +import yaml -def unipath(*paths): - '''os.path.join with forward slashes only.''' - - return os.path.abspath( - os.path.expanduser(os.path.join(*paths)) - ).replace('\\', '/') - - -def get_qualname(obj): - '''Get the qualified name (dotted path) of the object''' +# Local imports +from .compat import Mapping, Path, basestring - try: - return obj.__qualname__ - except AttributeError: - pass - - if inspect.ismethod(obj): - if obj.__self__: - cls = obj.__self__.__class__ - else: - cls = obj.im_class - return cls.__name__ + '.' + obj.__name__ - return get_callable_name(obj) - - -def pop_attr(obj, attr, default): - '''Get the value of an attr then delete it else return default''' - - try: - value = getattr(obj, attr) - delattr(obj, attr) - except AttributeError: - value = default - return value +__all__ = [ + 'import_file', + 'isolated_imports', + 'iter_modules', + 'unipath', + 'ensure_exists', + 'unload_modules', + 'get_lib_path', + 'update_env', + 'update_envvar', + 'update_dict' +] +package_path = Path(__file__).parent.resolve() -def get_callable_name(obj): - '''Get name of obj object or callable''' +def get_lib_path(): + lib = package_path.parent + if lib.name == 'construct': + # We're in develop mode so we check a construct dep instead + import entrypoints + return Path(entrypoints.__file__).parent + return lib - try: - return obj.func_name - except AttributeError: - return obj.__name__ - except AttributeError: - return obj.__class__.__name__ - except AttributeError: - return obj.__func__.__name__ +def yaml_dump(data, **kwargs): + kwargs.setdefault('allow_unicode', True) + kwargs.setdefault('encoding', 'utf-8') + kwargs.setdefault('default_flow_style', False) + return yaml.safe_dump(data, **kwargs) -def path_split(value): - if isinstance(value, basestring): - return [value] - elif ';' in value or ':' in value: - return value.split(os.pathsep) +def yaml_load(data): + return yaml.safe_load(data) - raise EnvironmentError('Can not split: ' + value) +def unipath(*paths): + return Path(*paths).expanduser().resolve() -def env_with_default(var, default, converter=None): - '''Get an environment variable or a default value.''' - if var in os.environ: - value = os.environ[var] - else: - value = default +def ensure_exists(*folders): + for folder in folders: + if not os.path.exists(str(folder)): + os.makedirs(str(folder)) - if converter: - value = converter(value) - return value +def update_dict(a, b): + '''Recursively update one dictionary with another.''' + for key, b_value in b.items(): + a_value = a.get(key, None) + if not isinstance(a_value, Mapping): + a[key] = b_value + elif isinstance(a_value, Mapping) and isinstance(b_value, Mapping): + update_dict(a_value, b_value) + else: + a[key] = b_value -def ensure_type(obj, type_, exc_type=TypeError): - if not issubclass(obj, type_): - raise exc_type(f('{obj} must be subclass of {type_}')) +def update_env(d, **values): + '''Updates an environment dict with the specified values. An environment + dict is like os.environ; it includes os.pathsep separators. -def ensure_instance(obj, type_, exc_type=TypeError): - if not isinstance(obj, type_): - raise exc_type(f('{obj} must be instance of {type_}')) + Behavior: + - List values are combined with the existing values in d. + - String values override values in d. + - Dicts containing platform keys ('win', 'linux', 'mac') are selected + based on the current platform. If the current platform is missing + the key will not be updated. + Example: + >>> import os + >>> env = os.environ.copy() + >>> update_env(env, PATH=['some_path'], VAR='value') -def ensure_callable(obj, exc_type=TypeError): - '''Ensures that an obj is callable''' + Arguments: + d (dict): Environment dict to update + **values: Values to update d with + ''' - if not callable(obj): - raise exc_type('{obj} must be callable') + for k, v in values.items(): + update_envvar(d, k, v) + # TODO: We can expand path values containing string template tokens like + # ${variable} here if we want. This would have to be expanded twice to + # handle values referencing other variables. -def update_dict(d, u): - '''Update a dict recursively. - See also: - https://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth - ''' +def update_envvar(d, k, v): + '''Used by update_env to update a single key with the specified value.''' + from .constants import PLATFORM - for k, v in u.items(): - if isinstance(v, Mapping): - dv = d.get(k, {}) - if isinstance(dv, Mapping): - d[k] = update_dict(dv, v) - else: - d[k] = v - else: + if isinstance(v, basestring): + d[k] = v + elif isinstance(v, list): + v = os.pathsep.join(v) + if k not in d: d[k] = v - return d + else: + d[k] = os.pathsep.join([v, d[k]]) + elif isinstance(v, dict): + # This is not a platform dict let's throw an error + if not set(['win', 'linux', 'mac']) & set(v.keys()): + # TODO: is this the correct thing to do? + raise ValueError( + 'Got dict expected one of: str, list, int, float' + ) + # Handle a platform dict + v = v.get(PLATFORM, None) + if v is not None: + update_envvar(d, k, v) + else: + d[k] = str(v) @contextmanager @@ -173,13 +148,13 @@ def isolated_imports(path=None, restore=True): sys.path[:] = old_path if restore: - for k, v in list(sys.modules.items()): + for k in list(sys.modules.keys()): if k in old_modules: sys.modules[k] = old_modules[k] else: del(sys.modules[k]) - for k, v in list(sys.path_importer_cache.items()): + for k in list(sys.path_importer_cache.keys()): if k in old_path_cache: sys.path_importer_cache[k] = old_path_cache[k] else: @@ -193,7 +168,7 @@ def unload_modules(*names): ''' for name in names: - for k, v in list(sys.modules.items()): + for k in list(sys.modules.keys()): if k.startswith(name): del(sys.modules[k]) @@ -206,10 +181,11 @@ def import_file(path, isolated=True): case yet, Extension loading. There have been no apparent downsides yet. ''' - root, basename = os.path.split(path) - name, _ = os.path.splitext(basename) + path = Path(path) + root = path.parent + name = path.stem - with isolated_imports(root, restore=isolated): + with isolated_imports(str(root), restore=isolated): unload_modules(name) mod = __import__(name, globals(), locals(), []) @@ -224,40 +200,31 @@ def iter_modules(*paths): ''' for path in paths: - for py_file in glob(path + '/*.py'): + path = Path(path) + for py_file in path.glob('*.py'): mod = import_file(py_file, isolated=False) yield mod - for py_pkg in glob(path + '/*/__init__.py'): - mod = import_file(os.path.dirname(py_pkg), isolated=False) + for py_pkg in path.glob('*/__init__.py'): + mod = import_file(py_pkg.parent, isolated=False) yield mod +def copy_file(src, dest): + '''Copy a source file to the specified location. + + Arguments: + src (str or Path): Source file to copy + dest (str or Path): Destination file or directory + ''' + shutil.copy(str(src), str(dest)) + + class classproperty(object): + '''Like a property but for a Class object.''' def __init__(self, fn): self.fn = fn def __get__(self, obj, type): return self.fn(type) - - -@contextmanager -def dummy_ctxmanager(): - yield None - - -class cached_property(object): - '''Like property but replaces the instance method with the result of the - first call''' - - def __init__(self, method): - self.method = method - - def __get__(self, obj, type): - if obj is None: - return self - - value = self.method(obj) - setattr(obj, self.method.__name__, value) - return value diff --git a/construct/vendor/__init__.py b/construct/vendor/__init__.py deleted file mode 100644 index af1d81c..0000000 --- a/construct/vendor/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -''' -Included 3rd Party packages -=========================== - -Lucidity --------- -Temporarily included until a Python 3 support is added upstream. - - Changed absolute imports to relative - - Fixed all Python 3 errors -''' diff --git a/construct/vendor/lucidity/__init__.py b/construct/vendor/lucidity/__init__.py deleted file mode 100644 index 74d1cfe..0000000 --- a/construct/vendor/lucidity/__init__.py +++ /dev/null @@ -1,128 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -import os -import uuid -import imp - -from ._version import __version__ -from .template import Template, Resolver -from .error import ParseError, FormatError, NotFound - - -def discover_templates(paths=None, recursive=True): - '''Search *paths* for mount points and load templates from them. - - *paths* should be a list of filesystem paths to search for mount points. - If not specified will try to use value from environment variable - :envvar:`LUCIDITY_TEMPLATE_PATH`. - - A mount point is a Python file that defines a 'register' function. The - function should return a list of instantiated - :py:class:`~lucidity.template.Template` objects. - - If *recursive* is True (the default) then all directories under a path - will also be searched. - - ''' - templates = [] - - if paths is None: - paths = os.environ.get('LUCIDITY_TEMPLATE_PATH', '').split(os.pathsep) - - for path in paths: - for base, directories, filenames in os.walk(path): - for filename in filenames: - _, extension = os.path.splitext(filename) - if extension != '.py': - continue - - module_path = os.path.join(base, filename) - module_name = uuid.uuid4().hex - module = imp.load_source(module_name, module_path) - try: - registered = module.register() - except AttributeError: - pass - else: - if registered: - templates.extend(registered) - - if not recursive: - del directories[:] - - return templates - - -def parse(path, templates): - '''Parse *path* against *templates*. - - *path* should be a string to parse. - - *templates* should be a list of :py:class:`~lucidity.template.Template` - instances in the order that they should be tried. - - Return ``(data, template)`` from first successful parse. - - Raise :py:class:`~lucidity.error.ParseError` if *path* is not - parseable by any of the supplied *templates*. - - ''' - for template in templates: - try: - data = template.parse(path) - except ParseError: - continue - else: - return (data, template) - - raise ParseError( - 'Path {0!r} did not match any of the supplied template patterns.' - .format(path) - ) - - -def format(data, templates): # @ReservedAssignment - '''Format *data* using *templates*. - - *data* should be a dictionary of data to format into a path. - - *templates* should be a list of :py:class:`~lucidity.template.Template` - instances in the order that they should be tried. - - Return ``(path, template)`` from first successful format. - - Raise :py:class:`~lucidity.error.FormatError` if *data* is not - formattable by any of the supplied *templates*. - - - ''' - for template in templates: - try: - path = template.format(data) - except FormatError: - continue - else: - return (path, template) - - raise FormatError( - 'Data {0!r} was not formattable by any of the supplied templates.' - .format(data) - ) - - -def get_template(name, templates): - '''Retrieve a template from *templates* by *name*. - - Raise :py:exc:`~lucidity.error.NotFound` if no matching template with - *name* found in *templates*. - - ''' - for template in templates: - if template.name == name: - return template - - raise NotFound( - '{0} template not found in specified templates.'.format(name) - ) diff --git a/construct/vendor/lucidity/_version.py b/construct/vendor/lucidity/_version.py deleted file mode 100644 index 86d9c8b..0000000 --- a/construct/vendor/lucidity/_version.py +++ /dev/null @@ -1,2 +0,0 @@ -__version__ = '1.5.0' - diff --git a/construct/vendor/lucidity/error.py b/construct/vendor/lucidity/error.py deleted file mode 100644 index 94b575e..0000000 --- a/construct/vendor/lucidity/error.py +++ /dev/null @@ -1,21 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -'''Custom error classes.''' - - -class ParseError(Exception): - '''Raise when a template is unable to parse a path.''' - - -class FormatError(Exception): - '''Raise when a template is unable to format data into a path.''' - - -class NotFound(Exception): - '''Raise when an item cannot be found.''' - - -class ResolveError(Exception): - '''Raise when a template reference can not be resolved.''' diff --git a/construct/vendor/lucidity/template.py b/construct/vendor/lucidity/template.py deleted file mode 100644 index c2cb759..0000000 --- a/construct/vendor/lucidity/template.py +++ /dev/null @@ -1,327 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2013 Martin Pengelly-Phillips -# :license: See LICENSE.txt. - -import abc -import six -import re -import functools -from collections import defaultdict - -from . import error - -# Type of a RegexObject for isinstance check. -_RegexType = type(re.compile('')) - - -class Template(object): - '''A template.''' - - _STRIP_EXPRESSION_REGEX = re.compile(r'{(.+?)(:(\\}|.)+?)}') - _PLAIN_PLACEHOLDER_REGEX = re.compile(r'{(.+?)}') - _TEMPLATE_REFERENCE_REGEX = re.compile(r'{@(?P.+?)}') - - ANCHOR_START, ANCHOR_END, ANCHOR_BOTH = (1, 2, 3) - - RELAXED, STRICT = (1, 2) - - def __init__(self, name, pattern, anchor=ANCHOR_START, - default_placeholder_expression='[\w_.\-]+', - duplicate_placeholder_mode=RELAXED, - template_resolver=None): - '''Initialise with *name* and *pattern*. - - *anchor* determines how the pattern is anchored during a parse. A - value of :attr:`~Template.ANCHOR_START` (the default) will match the - pattern against the start of a path. :attr:`~Template.ANCHOR_END` will - match against the end of a path. To anchor at both the start and end - (a full path match) use :attr:`~Template.ANCHOR_BOTH`. Finally, - ``None`` will try to match the pattern once anywhere in the path. - - *duplicate_placeholder_mode* determines how duplicate placeholders will - be handled during parsing. :attr:`~Template.RELAXED` mode extracts the - last matching value without checking the other values. - :attr:`~Template.STRICT` mode ensures that all duplicate placeholders - extract the same value and raises :exc:`~lucidity.error.ParseError` if - they do not. - - If *template_resolver* is supplied, use it to resolve any template - references in the *pattern* during operations. It should conform to the - :class:`Resolver` interface. It can be changed at any time on the - instance to affect future operations. - - ''' - super(Template, self).__init__() - self.duplicate_placeholder_mode = duplicate_placeholder_mode - self.template_resolver = template_resolver - - self._default_placeholder_expression = default_placeholder_expression - self._period_code = '_LPD_' - self._at_code = '_WXV_' - self._name = name - self._pattern = pattern - self._anchor = anchor - - # Check that supplied pattern is valid and able to be compiled. - self._construct_regular_expression(self.pattern) - - def __repr__(self): - '''Return unambiguous representation of template.''' - return '{0}(name={1!r}, pattern={2!r})'.format( - self.__class__.__name__, self.name, self.pattern - ) - - @property - def name(self): - '''Return name of template.''' - return self._name - - @property - def pattern(self): - '''Return template pattern.''' - return self._pattern - - def expanded_pattern(self): - '''Return pattern with all referenced templates expanded recursively. - - Raise :exc:`lucidity.error.ResolveError` if pattern contains a reference - that cannot be resolved by currently set template_resolver. - - ''' - return self._TEMPLATE_REFERENCE_REGEX.sub( - self._expand_reference, self.pattern - ) - - def _expand_reference(self, match): - '''Expand reference represented by *match*.''' - reference = match.group('reference') - - if self.template_resolver is None: - raise lucidity.error.ResolveError( - 'Failed to resolve reference {0!r} as no template resolver set.' - .format(reference) - ) - - template = self.template_resolver.get(reference) - if template is None: - raise lucidity.error.ResolveError( - 'Failed to resolve reference {0!r} using template resolver.' - .format(reference) - ) - - return template.expanded_pattern() - - def parse(self, path): - '''Return dictionary of data extracted from *path* using this template. - - Raise :py:class:`~lucidity.error.ParseError` if *path* is not - parsable by this template. - - ''' - # Construct regular expression for expanded pattern. - regex = self._construct_regular_expression(self.expanded_pattern()) - - # Parse. - parsed = {} - - match = regex.search(path) - if match: - data = {} - for key, value in sorted(match.groupdict().items()): - # Strip number that was added to make group name unique. - key = key[:-3] - - # If strict mode enabled for duplicate placeholders, ensure that - # all duplicate placeholders extract the same value. - if self.duplicate_placeholder_mode == self.STRICT: - if key in parsed: - if parsed[key] != value: - raise error.ParseError( - 'Different extracted values for placeholder ' - '{0!r} detected. Values were {1!r} and {2!r}.' - .format(key, parsed[key], value) - ) - else: - parsed[key] = value - - # Expand dot notation keys into nested dictionaries. - target = data - - parts = key.split(self._period_code) - for part in parts[:-1]: - target = target.setdefault(part, {}) - - target[parts[-1]] = value - - return data - - else: - raise error.ParseError( - 'Path {0!r} did not match template pattern.'.format(path) - ) - - def format(self, data): - '''Return a path formatted by applying *data* to this template. - - Raise :py:class:`~lucidity.error.FormatError` if *data* does not - supply enough information to fill the template fields. - - ''' - - format_specification = self._construct_format_specification( - self.expanded_pattern() - ) - - return self._PLAIN_PLACEHOLDER_REGEX.sub( - functools.partial(self._format, data=data), - format_specification - ) - - def _format(self, match, data): - '''Return value from data for *match*.''' - placeholder = match.group(1) - parts = placeholder.split('.') - - try: - value = data - for part in parts: - value = value[part] - - except (TypeError, KeyError): - raise error.FormatError( - 'Could not format data {0!r} due to missing key {1!r}.' - .format(data, placeholder) - ) - - else: - return value - - def keys(self): - '''Return unique set of placeholders in pattern.''' - format_specification = self._construct_format_specification( - self.expanded_pattern() - ) - return set(self._PLAIN_PLACEHOLDER_REGEX.findall(format_specification)) - - def references(self): - '''Return unique set of referenced templates in pattern.''' - format_specification = self._construct_format_specification( - self.pattern - ) - return set(self._TEMPLATE_REFERENCE_REGEX.findall(format_specification)) - - def _construct_format_specification(self, pattern): - '''Return format specification from *pattern*.''' - return self._STRIP_EXPRESSION_REGEX.sub('{\g<1>}', pattern) - - def _construct_regular_expression(self, pattern): - '''Return a regular expression to represent *pattern*.''' - # Escape non-placeholder components. - expression = re.sub( - r'(?P{(.+?)(:(\\}|.)+?)?})|(?P.+?)', - self._escape, - pattern - ) - - # Replace placeholders with regex pattern. - expression = re.sub( - r'{(?P.+?)(:(?P(\\}|.)+?))?}', - functools.partial( - self._convert, placeholder_count=defaultdict(int) - ), - expression - ) - - if self._anchor is not None: - if bool(self._anchor & self.ANCHOR_START): - expression = '^{0}'.format(expression) - - if bool(self._anchor & self.ANCHOR_END): - expression = '{0}$'.format(expression) - - # Compile expression. - try: - compiled = re.compile(expression) - except re.error as error: - if any([ - 'bad group name' in str(error), - 'bad character in group name' in str(error) - ]): - message = 'Placeholder name contains invalid characters.' - else: - message = 'Invalid pattern: ' + str(error) - - six.raise_from(ValueError(message), error) - - return compiled - - def _convert(self, match, placeholder_count): - '''Return a regular expression to represent *match*. - - *placeholder_count* should be a `defaultdict(int)` that will be used to - store counts of unique placeholder names. - - ''' - placeholder_name = match.group('placeholder') - - # Support at symbol (@) as referenced template indicator. Currently, - # this symbol not a valid character for a group name in the standard - # Python regex library. Rather than rewrite or monkey patch the library - # work around the restriction with a unique identifier. - placeholder_name = placeholder_name.replace('@', self._at_code) - - # Support period (.) as nested key indicator. Currently, a period is - # not a valid character for a group name in the standard Python regex - # library. Rather than rewrite or monkey patch the library work around - # the restriction with a unique identifier. - placeholder_name = placeholder_name.replace('.', self._period_code) - - # The re module does not support duplicate group names. To support - # duplicate placeholder names in templates add a unique count to the - # regular expression group name and strip it later during parse. - placeholder_count[placeholder_name] += 1 - placeholder_name += '{0:03d}'.format( - placeholder_count[placeholder_name] - ) - - expression = match.group('expression') - if expression is None: - expression = self._default_placeholder_expression - - # Un-escape potentially escaped characters in expression. - expression = expression.replace('\{', '{').replace('\}', '}') - - return r'(?P<{0}>{1})'.format(placeholder_name, expression) - - def _escape(self, match): - '''Escape matched 'other' group value.''' - groups = match.groupdict() - if groups['other'] is not None: - return re.escape(groups['other']) - - return groups['placeholder'] - - -# Python 2 and 3 compatible ABCMeta -ABC = abc.ABCMeta('ABC', (object,), {}) - - -class Resolver(ABC): - '''Template resolver interface.''' - - @abc.abstractmethod - def get(self, template_name, default=None): - '''Return template that matches *template_name*. - - If no template matches then return *default*. - - ''' - return default - - @classmethod - def __subclasshook__(cls, subclass): - '''Return whether *subclass* fulfils this interface.''' - if cls is Resolver: - return callable(getattr(subclass, 'get', None)) - - return NotImplemented diff --git a/docs/Makefile b/docs/Makefile index e6b851a..94c6cf7 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -SPHINXPROJ = fsfs +SPHINXPROJ = construct SOURCEDIR = . BUILDDIR = . diff --git a/docs/api.rst b/docs/api.rst index 3f5a21e..39a20fb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,137 +1,87 @@ +================= API Documentation ================= -.. automodule:: construct.api - :members: - :undoc-members: - :show-inheritance: - - -construct\.action module ------------------------- - -.. automodule:: construct.action - :members: - :undoc-members: - :show-inheritance: - -construct\.actioncontext module ---------------------------- +API Function +------------ +.. autofunction:: construct.API -.. automodule:: construct.actioncontext +API Object +---------- +.. autoclass:: construct.api.API :members: - :undoc-members: :show-inheritance: -construct\.actionparams module ------------------------------- - -.. automodule:: construct.actionparams - :members: - :undoc-members: - :show-inheritance: - -construct\.actionrunner module ----------------------------- - -.. automodule:: construct.actionrunner +Context +------- +.. autoclass:: construct.context.Context :members: - :undoc-members: :show-inheritance: +Builtin Events +-------------- +The following events are builtin and may be useful in extending Construct. -construct\.compat module ------------------------- ++--------------+-----------+-----------------------------------+ +| event | arguments | description | ++==============+===========+===================================+ +| before_setup | api | Sent before api initialization. | ++--------------+-----------+-----------------------------------+ +| after_setup | api | Sent after api initialization. | ++--------------+-----------+-----------------------------------+ +| before_close | api | Sent before api uninitialization. | ++--------------+-----------+-----------------------------------+ +| after_close | api | Sent after api uninitialization. | ++--------------+-----------+-----------------------------------+ -.. automodule:: construct.compat +EventManager +------------ +.. autoclass:: construct.events.EventManager :members: - :undoc-members: :show-inheritance: -construct\.config module ---------------------------- - -.. automodule:: construct.config +Settings +-------- +.. automodule:: construct.settings :members: - :undoc-members: :show-inheritance: -construct\.constants module ---------------------------- - -.. automodule:: construct.constants +Path +---- +.. autoclass:: construct.path.Path :members: - :undoc-members: :show-inheritance: -construct\.context module -------------------------- - -.. automodule:: construct.context +Utils +----- +.. automodule:: construct.utils :members: - :undoc-members: :show-inheritance: -construct\.errors module ---------------------- - +Errors +------ .. automodule:: construct.errors :members: - :undoc-members: - :show-inheritance: - -construct\.extension module -------------------------- - -.. automodule:: construct.extension - :members: - :undoc-members: - :show-inheritance: - -construct\.models module ------------------------- - -.. automodule:: construct.models - :members: - :undoc-members: :show-inheritance: -construct\.signals module ------------------------- - -.. automodule:: construct.signals +Extension Types +--------------- +.. autoclass:: construct.extensions.Extension :members: - :undoc-members: :show-inheritance: -construct\.stats module ------------------------- - -.. automodule:: construct.stats +.. autoclass:: construct.extensions.Host :members: - :undoc-members: :show-inheritance: -construct\.tasks module ------------------------ - -.. automodule:: construct.tasks +ExtensionManager +---------------- +.. autoclass:: construct.extensions.ExtensionManager :members: - :undoc-members: :show-inheritance: -construct\.types module ------------------------ - -.. automodule:: construct.types - :members: - :undoc-members: - :show-inheritance: - -construct\.utils module ----------------------- - -.. automodule:: construct.utils +Builtin Extensions +------------------ +.. autoclass:: construct.extensions.software.Software :members: - :undoc-members: :show-inheritance: diff --git a/docs/cli.rst b/docs/cli.rst deleted file mode 100644 index 7456a44..0000000 --- a/docs/cli.rst +++ /dev/null @@ -1,107 +0,0 @@ -====================== -Command Line Interface -====================== - -.. code-block:: console - - $ construct - Usage: construct [options] - - Current Context - host cli - root ~/projects - user dbradham - platform win - - Commands - version Version information - home Go to root directory - pop Go back to last location - push Go to first search result - search Search for Entries - read Read metadata - write Write metadata - tag Tag a directory - untag Untag a directory - - Actions - new.project Create a new Project - - Options: - -h, --help show this help message and exit (default: False) - -v, --verbose verbose output (default: False) - - -Anatomy of the CLI -================== - -Current Context - Describes the context of your current working directory. - -Commands - These are universal commands that are always present. They provide core functionality like navigation, searching, reading and writing data, tagging and untagging directories, and version info about Construct. - -Actions - Actions are invoked the same way commands are, but, they differ in a few ways. Actions are contextual, so different Actions will be available depending on your current context. For example, the new.project Action is available when you are not in a project. When you are in a project, you'll have actions for creating assets, shots, tasks and launching application. - - Actions are also typically composed of several tasks that can have a complicated flow of execution. In turn, when called from the CLI detailed info on the execution of the Actions tasks is provided including any artifacts that were created. An artifact is something produced by an Action. For example, the new.project action creates a new folder on your file system containing some metadata. - -Options - Options are data you provide to a command or action. Actions frequently require options to produce the results you want. For example, the new.project Action requires a --root option that specifies the directory where the project should be created. The --help option is universal, you can provide it whenever you need help using the CLI. - -Navigation Commands -=================== -The construct cli has 4 main commands to help you navigate your projects. - -home - Navigates to your context's root directory. Usually where your projects - are located. -push - Navigates to the first result in a search by name or tag -pop - Navigates -search - Finds Entries by name or tag. - - -Create a new Project -==================== -Construct has several builtin actions that can be used to create and manage your projects. Let's use the new.project, new.asset, and new.task Actions to build a new project. By the end of this section we will have created a new project named "my_project" with an asset named "coffee_cup" that contains a model task. - -First we'll start by going to our construct root directory. - -.. code-block:: console - - $ construct home - ~/projects$ - -Now we can use new.project to create our project. - -.. code-block:: console - - ~/projects$ construct new.project --root my_project - ... - Atrifacts - project ~/projects/my_project - -You should see a nice and detailed report ending with an Artifacts section showing us the the new project that was created. Now let's dive into my_project and create our asset and tasks. - -.. code-block:: console - - ~/projects$ construct search my_project - ~/projects/my_project - - ~/projects$ construct push my_project - ~/projects/my_project$ construct new.asset --name coffee_cup --asset_type prop - ... - Artifacts - asset ~/projects/my_project/assets/prop/coffee_cup - - ~/projects/my_project$ construct push cup - ~/projects/my_project/assets/prop/coffee_cup$ construct new.task --type model - ... - Artifacts - task ~/projects/my_project/assets/prop/coffee_cup/model - - ~/projects/my_project/assets/prop/coffee_cup$ construct push model - ~/projects/my_project/assets/prop/coffee_cup/model$ diff --git a/docs/concepts.rst b/docs/concepts.rst index 5e66e3a..6a56836 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -1,127 +1,16 @@ ======== Concepts ======== +# TODO Context ======= -Construct has a strong sense of Context. The current Context is used to -determine which Extensions, Actions, Tasks and Templates are available. +# TODO -:host: Host application cli, maya, nuke, etc... -:platform: win, linux, or mac -:root: Directory containing your projects -:user: Username -:project: Project Entry -:sequence: Sequence Entry - contains shots -:shot: Shot Entry - contains tasks -:asset_type: AssetType Entry - contains assets -:asset: Asset Entry - contains tasks -:task: Task Entry - where publishes and workspaces are stored -:workspace: Workspace Entry - where work files are stored -:file: Current workfile file +Shots are Assets +================ +# TODO - -All Extensions, Actions, and Tasks implement an :meth:`available` method that -takes a Context object and returns True if available in that Context. - - -Extensions -========== -Extensions are the basic building blocks of Construct. They provide Actions, -Tasks, and Templates. All builtin actions and templates are provided by the -Builtins Extension. Extensions can provide Tasks to extend existing Actions or -provide their own Actions. - - -.. code-block:: python - - from construct import Extension - - class MyExtension(Extension): - - name = 'My Extension' - attr_name = 'my_extension' - - def load(self): - # setup extension - # add Actions, Tasks, and template paths - pass - - def available(self, ctx): - # Return True if Extension is available in ctx - return True - - -Actions -======= -Actions are interfaces to run a set of Tasks ordered by their respective -priorities and requirements. Let's take a look at the :class:`NewProject` Action provided by the Builtins Extension. - - -.. code-block:: python - - import construct - from construct import Action - - class NewProject(Action): - '''Create a new Project''' - - label = 'New Project' - identifier = 'new.project' - - @staticmethod - def parameters(ctx): - params = dict( - root={ - 'label': 'Project Root', - 'required': True, - 'type': types.String, - 'help': 'project root directory', - }, - template={ - 'label': 'Project Template', - 'required': True, - 'type': types.String, - 'help': 'name of a project template', - } - ) - - if not ctx: - return params - - templates = list(construct.get_templates('project').keys()) - params['template']['options'] = templates - if templates: - params['template']['default'] = templates[0] - - return params - - @staticmethod - def available(ctx): - return not ctx.project - - -Tasks -===== -Tasks are decorated python functions that are added to an Action and ordered by -their respective priorities and requirements. - -.. code-block:: python - - from construct.tasks import task, requires, success, returns, artifact - - @task - @requires(success('other_task')) - @returns(artifact('message')) - def some_task(): - return 'other_task completed successfully' - -This task is useless, but, it shows us a few neat things about Tasks. Our task -'some_task' requires the success of 'other_task' and returns an artifact named -'message'. Construct provides a rich set of decorators and functions to help -you setup the flow of your Actions. Tasks also remain callable after being -decorated, so you can run them on their own which is nice for testing. - -.. code-block:: python - - assert some_task() == 'other_task completed successfully' +Asset Libraries Are Projects Too +================================ +# TODO diff --git a/docs/conf.py b/docs/conf.py index 7333521..09c00a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,12 +1,18 @@ # -*- coding: utf-8 -*- -import sphinx_rtd_theme +# Standard library imports import os import sys +# Third party imports +import sphinx_rtd_theme + +# Local imports +import construct + + docs_root = os.path.dirname(__file__) lib_root = os.path.dirname(docs_root) sys.path.insert(1, lib_root) -import construct project = construct.__title__ copyright = '2018, ' + construct.__author__ diff --git a/docs/configuration.rst b/docs/configuration.rst index bd85fa6..e59a866 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1,149 +1,197 @@ ============= Configuration ============= +When starting up Construct will look for a construct.yaml file in the following locations: -Construct uses yaml files for configuration. Here is the default configuration, -construct/defaults/construct.yaml. You can specify your own configuration by -creating your own yaml file, and pointing your CONSTRUCT_CONFIG environment -variable to it. +1. Paths stored in the CONSTRUCT_PATH environment variable +2. Your USER_PATH ``~/.construct`` +The first construct.yaml that is found becomes your settings folder. If no construct.yaml file is found a default settings structure is created in the last path checked (default: `~/.construct`). The settings folder contains user icons, extensions, templates and software configurations. -.. code-block:: yaml +To share a configuration accross your stuiod set CONSTRUCT_PATH on your workstations to a network folder. You can also override the default lookup behavior by providing your own list of paths when initializing the Construct API. - ROOT: '~/projects' - EXTENSION_PATHS: - - '~/.construct' - EVENT_STREAM_DB: '~/.construct/eventstream.sqlite' - DISABLE_EXTENSIONS: [] +:: - LOGGING: + import construct + api = construct.API(path=['~/.custom_path']) + +.. note:: Settings files are validated prior to being loaded. This prevents Construct from running with faulty settings. + +Settings folder structure +========================= + +:: + + extensions/ + icons/ + templates/ + software/ + construct.yaml + +extensions folder +----------------- +Python modules and packages containing extensions folders. This is the location where Construct discovers and loads Extensions. + +icons folder +------------ +One of the locations where Construct will look for icons. When configuring software you can place custom +icons here and reference them using `icons/myicon.png`. + +templates folder +---------------- +Template files like a maya workspace.mel. These templates can be used in software configurations. + +software folder +--------------- +Individual software configuration files. + +construct.yaml +-------------- +The main settings file. By default it looks like this. +:: + + locations: + local: + lib: ~/lib + projects: ~/projects + my_location: local + my_mount: projects + extensions: [] + logging: version: 1 formatters: simple: - format: '%(levelname).1s:%(name)s> %(message)s' + format: '%(levelname).1s:%(name)s> %(message)s' handlers: console: - formatter: simple class: logging.StreamHandler + formatter: simple loggers: construct: + handlers: + - console level: DEBUG - handlers: [console] - propagate: False - - - STATUSES: - - 'waiting' - - 'in progress' - - 'rendering' - - 'pending' - - 'rejected' - - 'complete' - - - TASK_TYPES: - - model - - rig - - shade - - texture - - comp - - light - - layout - - - PATH_TEMPLATES: - asset_type: '{project}/assets/{asset_type}' - asset: '{project}/assets/{asset_type}/{asset}' - sequence: '{project}/sequences/{sequence}' - shot: '{project}/sequences/{sequence}/{shot}' - task: '{parent}/{task}' - publish: '{task}/publish/{file_type}/{task}_{name}/v{:0>3d}' - publish_file: '{task}_{name}_v{:0>3d}.{ext}' - workspace: '{task}/work/{workspace}' - workspace_file: '{task}_{name}_v{:0>3d}.{ext}' - - - SOFTWARE: - maya2017: - host: 'maya' - workspace: 'maya' - identifier: 'launch.maya2017' - description: 'Launch Autodesk Maya2017' - default_workspace: 'maya' - icon: './icons/maya.png' - cmd: - win: - path: C:/Program Files/Autodesk/Maya2017/bin/maya.exe - args: [] - linux: - path: /usr/autodesk/maya2017/bin/maya - args: [] - mac: - path: /Applications/Autodesk/maya2017/bin/maya - args: [] - maya2018: - host: 'maya' - identifier: 'launch.maya2018' - description: 'Launch Autodesk Maya2018' - default_workspace: 'maya' - icon: './icons/maya.png' - extensions: ['.ma', '.mb', '.fbx', '.obj', '.abc'] - cmd: - win: - path: C:/Program Files/Autodesk/Maya2018/bin/maya.exe - args: [] - linux: - path: /usr/autodesk/maya2018/bin/maya - args: [] - mac: - path: /Applications/Autodesk/maya2018/bin/maya - args: [] - nuke11.1v2: - host: 'nuke' - identifier: 'launch.nuke11.1v2' - description: 'Launch The Foundry Nuke11.1v2' - default_workspace: 'nuke' - icon: './icons/nuke.png' - extensions: ['.nk'] - cmd: - win: - path: C:/Program Files/Nuke11.1v2/Nuke11.1.exe - args: [] - linux: - path: /usr/local/Nuke11.1v2/Nuke11.1 - args: [] - mac: - path: /Applications/Nuke11.1v2/Nuke11.1v2.app/Nuke11.1 - args: [] - sublime: - host: 'sublime' - identifier: 'launch.sublimetext' - description: 'Launch Sublimt Text 3' - default_workspace: null - icon: './icons/sublime.png' - extensions: ['.txt', '.py', '.mel'] - cmd: - win: - path: C:/Program Files/Sublime Text 3//subl.exe - args: [] - linux: - path: /usr/bin/subl - args: [] - mac: - path: /Applications/Sublime Text.app/Contents/SharedSupport/bin/subl - args: [] - - - FILE_TYPES: - alembic: - extensions: ['.abc'] - maya: - extensions: ['.ma', '.mb'] - nuke: - extensions: ['.nk'] - c4d: - extensions: ['.c4d'] - photoshop: - extensions: ['.psd', '.psb'] - illustrator: - extensions: ['.ai'] + + +Builtin Settings +================ +The builtin settings that must be included in your construct.yaml file. + +locations +--------- +A dictionary containing locations and file system mounts. These are used to lookup file system paths to projects, folders, assets, and files. The default locations value supports a simple local setup. + +:: + + locations: + local: + lib: ~/lib + projects: ~/projects + +For a multisite solution you can define multiple locations and OS specific mounts. + +:: + + locations: + NY: + lib: + win: //ny-server/lib + mac: /Volumes/ny-server/lib + linux: /mnt/ny-server/lib + projects: + win: //ny-server/projects + mac: /Volumes/ny-server/projects + linux: /mnt/ny-server/projects + LA: + lib: + win: //la-server/lib + mac: /Volumes/la-server/lib + linux: /mnt/la-server/lib + projects: + win: //la-server/projects + mac: /Volumes/la-server/projects + linux: /mnt/la-server/projects + +my_location +----------- +The default location that Construct will use. + +Default: ``local`` + +my_mount +-------- +The default mount that Construct will use. + +Default: ``projects`` + +extensions +---------- +A list of extensions to import and load when Construct starts. This is where you would list extensions +installed using pip. + +.. note:: Builtin Extensions and Extensions located in your settings `extensions` folder will be loaded automatically, they do not need to be included here. + +default: ``[]`` + +logging +------- +A dictConfig used to configure logging. + + +Software Settings +================= +Software is configured in individual yaml files stored in the software settings folder. Let's take a look at how we would configure Autodesk Maya2019. + +software/maya2019.yaml:: + + label: Autodesk Maya 2019 + icon: icons/maya.png + host: maya + cmd: + linux: /usr/Autodesk/Maya2019/bin/maya + mac: /applications/Autodesk/Maya2019.app/bin/maya.exe + win: C:/Program Files/Autodesk/Maya2019/bin/maya.exe + args: [] + files: + templates/workspace.mel: workspace.mel + folders: + - data + - cache + extensions: ['.ma', '.mb', '.fbx', '.abc', '.obj'] + env: {} + +label +----- +The nice name for the software. + +icon +---- +The path to an icon relative to your settings folder. + +host +---- +The identifier of a ``Host`` extension that handles this software. Usually you will have to write your own ``Host`` extensions to support any meaningful integration with a piece of software. + +cmd +--- +OS specific paths to the software executables. + +args +---- +Args to pass to the software executable. + +files +----- +Files to copy to the workspace root when created. Each key is the path to a template file and it's accompanying value is the path relative to the workspace root where it will be copied to. + +folders +------- +A list of folders to create in the workspace root. + +extensions +---------- +A list of extensions that this software is able to open. + +env +--- +Environment variables to set when launching this software. When values are lists they will be prepended to your current Environment variables value. When values are strings they will override an existing Environment variable value. diff --git a/docs/doctrees/api.doctree b/docs/doctrees/api.doctree index 4b9eb22..595c53f 100644 Binary files a/docs/doctrees/api.doctree and b/docs/doctrees/api.doctree differ diff --git a/docs/doctrees/cli.doctree b/docs/doctrees/cli.doctree deleted file mode 100644 index 79dd915..0000000 Binary files a/docs/doctrees/cli.doctree and /dev/null differ diff --git a/docs/doctrees/concepts.doctree b/docs/doctrees/concepts.doctree index 76b9e5a..9d1e245 100644 Binary files a/docs/doctrees/concepts.doctree and b/docs/doctrees/concepts.doctree differ diff --git a/docs/doctrees/configuration.doctree b/docs/doctrees/configuration.doctree index 530144c..3675f4b 100644 Binary files a/docs/doctrees/configuration.doctree and b/docs/doctrees/configuration.doctree differ diff --git a/docs/doctrees/environment.doctree b/docs/doctrees/environment.doctree index 55fc47b..ce2461f 100644 Binary files a/docs/doctrees/environment.doctree and b/docs/doctrees/environment.doctree differ diff --git a/docs/doctrees/environment.pickle b/docs/doctrees/environment.pickle index f1e1630..afbb457 100644 Binary files a/docs/doctrees/environment.pickle and b/docs/doctrees/environment.pickle differ diff --git a/docs/doctrees/guide.doctree b/docs/doctrees/guide.doctree index 5bf655d..52f5506 100644 Binary files a/docs/doctrees/guide.doctree and b/docs/doctrees/guide.doctree differ diff --git a/docs/doctrees/index.doctree b/docs/doctrees/index.doctree index 12c683b..94d354a 100644 Binary files a/docs/doctrees/index.doctree and b/docs/doctrees/index.doctree differ diff --git a/docs/environment.rst b/docs/environment.rst index 661a29c..5bd3bae 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -1,11 +1,3 @@ ===================== Environment Variables ===================== - - -CONSTRUCT_ROOT - Root directory containing your projects. Defaults to `~/projects`. - -CONSTRUCT_CONFIG - Path to your Construct config file. - If not set Construct will use `construct/defaults/construct.yaml`. diff --git a/docs/guide.rst b/docs/guide.rst index 8bf2f92..b2caeff 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -1,150 +1,20 @@ ========= API Guide ========= +# TODO -Initialize Construct -==================== -Before we can do anything useful with construct, we need to initialize it. +Quickstart +========== +# TODO -.. code-block:: python - - import construct - construct.init() - -The initialization process performs the following actions to configure -construct. - -1. loads default config or config file set by CONSTRUCT_CONFIG env var -2. Configures logging for the "construct" namespace -3. Builds Context from your environment variables -4. Discovers Extensions in the following locations - - - construct.extensions entry_point - - paths passed to construct.init - - paths set in EXTENSION_PATHS config key - - paths defined by CONSTRUCT_EXTENSION_PATHS env var - -5. Loads Builtins Extension -6. Configures fsfs - -Create a Project +Builtin Extensions ================== +# TODO -.. code-block:: python - - my_project = construct.new_project( - root='~/projects/my_project', - template='vfx_project' - ) - - -Create a Shot with an animation task -==================================== - -.. code-block:: python - - seq010 = construct.new_sequence( - project=my_project, - name='seq010', - template='vfx_sequence' - ) - sh010 = construct.new_shot( - sequence=seq010, - name='sh010', - template='vfx_shot' - ) - sh010_anim = construct.new_task( - parent=sh010, - type='animation' - ) - - -Create an Asset with a model task -================================= - -.. code-block:: python - - coffee_cup = construct.new_asset( - project=my_project, - asset_type='prop', - name='coffee_cup', - template='vfx_asset' - ) - coffee_cup_model = construct.new_task( - parent=coffee_cup, - type='model' - template='vfx_task' - ) - - -Working with Context -==================== -You can set the current context from a path... - -.. code-block:: python - - construct.set_context_from_path('~/projects/my_project') - -or from an Entry. - -.. code-block:: python - - my_project = construct.search('my_project').one() - construct.set_context_from_entry(my_project) - -Get the Actions available in your current context... - -.. code-block:: python - - available_actions = construct.actions.collect() - -or in a specific context. - -.. code-block:: python - - ctx = construct.Context.from_path('~/projects/other_project') - other_actions = construct.actions.collect(ctx) - - -Searching -========= - -Find all projects - -.. code-block:: python - - all_projects = construct.search(tags=['project']) - -Find one project - -.. code-block:: python - - project = construct.search('my_project', tags=['project']).one() - -Find all assets in a project - -.. code-block:: python - - assets = project.assets - - -Find all model tasks in a project - -.. code-block:: python - - model_tasks = project.children().tags('model', 'task') - - -Find all shots in a sequence - -.. code-block:: python - - seq = project.sequences.name('seq010').one() - shots = seq.shots - - -Find a nested Entry using a name selector - -.. code-block:: python +Extending Construct +=================== +# TODO - model_task = construct.search('my_project/coffee_cup/model') +Examples +======== +# TODO diff --git a/docs/html/.buildinfo b/docs/html/.buildinfo index f40fe25..e111ed8 100644 --- a/docs/html/.buildinfo +++ b/docs/html/.buildinfo @@ -1,4 +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: 1122801e1b5052b1df7f3bf688c72775 +config: 1ef6500821213e8d3a3ef0812f4eb270 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/html/.doctrees/api.doctree b/docs/html/.doctrees/api.doctree new file mode 100644 index 0000000..6b71300 Binary files /dev/null and b/docs/html/.doctrees/api.doctree differ diff --git a/docs/html/.doctrees/concepts.doctree b/docs/html/.doctrees/concepts.doctree new file mode 100644 index 0000000..5502737 Binary files /dev/null and b/docs/html/.doctrees/concepts.doctree differ diff --git a/docs/html/.doctrees/configuration.doctree b/docs/html/.doctrees/configuration.doctree new file mode 100644 index 0000000..28e3edb Binary files /dev/null and b/docs/html/.doctrees/configuration.doctree differ diff --git a/docs/html/.doctrees/environment.doctree b/docs/html/.doctrees/environment.doctree new file mode 100644 index 0000000..fdd8faf Binary files /dev/null and b/docs/html/.doctrees/environment.doctree differ diff --git a/docs/html/.doctrees/environment.pickle b/docs/html/.doctrees/environment.pickle new file mode 100644 index 0000000..615e0bb Binary files /dev/null and b/docs/html/.doctrees/environment.pickle differ diff --git a/docs/html/.doctrees/guide.doctree b/docs/html/.doctrees/guide.doctree new file mode 100644 index 0000000..7d20cfd Binary files /dev/null and b/docs/html/.doctrees/guide.doctree differ diff --git a/docs/html/.doctrees/index.doctree b/docs/html/.doctrees/index.doctree new file mode 100644 index 0000000..3057d7d Binary files /dev/null and b/docs/html/.doctrees/index.doctree differ diff --git a/docs/html/_sources/api.rst.txt b/docs/html/_sources/api.rst.txt index 3f5a21e..39a20fb 100644 --- a/docs/html/_sources/api.rst.txt +++ b/docs/html/_sources/api.rst.txt @@ -1,137 +1,87 @@ +================= API Documentation ================= -.. automodule:: construct.api - :members: - :undoc-members: - :show-inheritance: - - -construct\.action module ------------------------- - -.. automodule:: construct.action - :members: - :undoc-members: - :show-inheritance: - -construct\.actioncontext module ---------------------------- +API Function +------------ +.. autofunction:: construct.API -.. automodule:: construct.actioncontext +API Object +---------- +.. autoclass:: construct.api.API :members: - :undoc-members: :show-inheritance: -construct\.actionparams module ------------------------------- - -.. automodule:: construct.actionparams - :members: - :undoc-members: - :show-inheritance: - -construct\.actionrunner module ----------------------------- - -.. automodule:: construct.actionrunner +Context +------- +.. autoclass:: construct.context.Context :members: - :undoc-members: :show-inheritance: +Builtin Events +-------------- +The following events are builtin and may be useful in extending Construct. -construct\.compat module ------------------------- ++--------------+-----------+-----------------------------------+ +| event | arguments | description | ++==============+===========+===================================+ +| before_setup | api | Sent before api initialization. | ++--------------+-----------+-----------------------------------+ +| after_setup | api | Sent after api initialization. | ++--------------+-----------+-----------------------------------+ +| before_close | api | Sent before api uninitialization. | ++--------------+-----------+-----------------------------------+ +| after_close | api | Sent after api uninitialization. | ++--------------+-----------+-----------------------------------+ -.. automodule:: construct.compat +EventManager +------------ +.. autoclass:: construct.events.EventManager :members: - :undoc-members: :show-inheritance: -construct\.config module ---------------------------- - -.. automodule:: construct.config +Settings +-------- +.. automodule:: construct.settings :members: - :undoc-members: :show-inheritance: -construct\.constants module ---------------------------- - -.. automodule:: construct.constants +Path +---- +.. autoclass:: construct.path.Path :members: - :undoc-members: :show-inheritance: -construct\.context module -------------------------- - -.. automodule:: construct.context +Utils +----- +.. automodule:: construct.utils :members: - :undoc-members: :show-inheritance: -construct\.errors module ---------------------- - +Errors +------ .. automodule:: construct.errors :members: - :undoc-members: - :show-inheritance: - -construct\.extension module -------------------------- - -.. automodule:: construct.extension - :members: - :undoc-members: - :show-inheritance: - -construct\.models module ------------------------- - -.. automodule:: construct.models - :members: - :undoc-members: :show-inheritance: -construct\.signals module ------------------------- - -.. automodule:: construct.signals +Extension Types +--------------- +.. autoclass:: construct.extensions.Extension :members: - :undoc-members: :show-inheritance: -construct\.stats module ------------------------- - -.. automodule:: construct.stats +.. autoclass:: construct.extensions.Host :members: - :undoc-members: :show-inheritance: -construct\.tasks module ------------------------ - -.. automodule:: construct.tasks +ExtensionManager +---------------- +.. autoclass:: construct.extensions.ExtensionManager :members: - :undoc-members: :show-inheritance: -construct\.types module ------------------------ - -.. automodule:: construct.types - :members: - :undoc-members: - :show-inheritance: - -construct\.utils module ----------------------- - -.. automodule:: construct.utils +Builtin Extensions +------------------ +.. autoclass:: construct.extensions.software.Software :members: - :undoc-members: :show-inheritance: diff --git a/docs/html/_sources/cli.rst.txt b/docs/html/_sources/cli.rst.txt deleted file mode 100644 index 7456a44..0000000 --- a/docs/html/_sources/cli.rst.txt +++ /dev/null @@ -1,107 +0,0 @@ -====================== -Command Line Interface -====================== - -.. code-block:: console - - $ construct - Usage: construct [options] - - Current Context - host cli - root ~/projects - user dbradham - platform win - - Commands - version Version information - home Go to root directory - pop Go back to last location - push Go to first search result - search Search for Entries - read Read metadata - write Write metadata - tag Tag a directory - untag Untag a directory - - Actions - new.project Create a new Project - - Options: - -h, --help show this help message and exit (default: False) - -v, --verbose verbose output (default: False) - - -Anatomy of the CLI -================== - -Current Context - Describes the context of your current working directory. - -Commands - These are universal commands that are always present. They provide core functionality like navigation, searching, reading and writing data, tagging and untagging directories, and version info about Construct. - -Actions - Actions are invoked the same way commands are, but, they differ in a few ways. Actions are contextual, so different Actions will be available depending on your current context. For example, the new.project Action is available when you are not in a project. When you are in a project, you'll have actions for creating assets, shots, tasks and launching application. - - Actions are also typically composed of several tasks that can have a complicated flow of execution. In turn, when called from the CLI detailed info on the execution of the Actions tasks is provided including any artifacts that were created. An artifact is something produced by an Action. For example, the new.project action creates a new folder on your file system containing some metadata. - -Options - Options are data you provide to a command or action. Actions frequently require options to produce the results you want. For example, the new.project Action requires a --root option that specifies the directory where the project should be created. The --help option is universal, you can provide it whenever you need help using the CLI. - -Navigation Commands -=================== -The construct cli has 4 main commands to help you navigate your projects. - -home - Navigates to your context's root directory. Usually where your projects - are located. -push - Navigates to the first result in a search by name or tag -pop - Navigates -search - Finds Entries by name or tag. - - -Create a new Project -==================== -Construct has several builtin actions that can be used to create and manage your projects. Let's use the new.project, new.asset, and new.task Actions to build a new project. By the end of this section we will have created a new project named "my_project" with an asset named "coffee_cup" that contains a model task. - -First we'll start by going to our construct root directory. - -.. code-block:: console - - $ construct home - ~/projects$ - -Now we can use new.project to create our project. - -.. code-block:: console - - ~/projects$ construct new.project --root my_project - ... - Atrifacts - project ~/projects/my_project - -You should see a nice and detailed report ending with an Artifacts section showing us the the new project that was created. Now let's dive into my_project and create our asset and tasks. - -.. code-block:: console - - ~/projects$ construct search my_project - ~/projects/my_project - - ~/projects$ construct push my_project - ~/projects/my_project$ construct new.asset --name coffee_cup --asset_type prop - ... - Artifacts - asset ~/projects/my_project/assets/prop/coffee_cup - - ~/projects/my_project$ construct push cup - ~/projects/my_project/assets/prop/coffee_cup$ construct new.task --type model - ... - Artifacts - task ~/projects/my_project/assets/prop/coffee_cup/model - - ~/projects/my_project/assets/prop/coffee_cup$ construct push model - ~/projects/my_project/assets/prop/coffee_cup/model$ diff --git a/docs/html/_sources/concepts.rst.txt b/docs/html/_sources/concepts.rst.txt index 5e66e3a..6a56836 100644 --- a/docs/html/_sources/concepts.rst.txt +++ b/docs/html/_sources/concepts.rst.txt @@ -1,127 +1,16 @@ ======== Concepts ======== +# TODO Context ======= -Construct has a strong sense of Context. The current Context is used to -determine which Extensions, Actions, Tasks and Templates are available. +# TODO -:host: Host application cli, maya, nuke, etc... -:platform: win, linux, or mac -:root: Directory containing your projects -:user: Username -:project: Project Entry -:sequence: Sequence Entry - contains shots -:shot: Shot Entry - contains tasks -:asset_type: AssetType Entry - contains assets -:asset: Asset Entry - contains tasks -:task: Task Entry - where publishes and workspaces are stored -:workspace: Workspace Entry - where work files are stored -:file: Current workfile file +Shots are Assets +================ +# TODO - -All Extensions, Actions, and Tasks implement an :meth:`available` method that -takes a Context object and returns True if available in that Context. - - -Extensions -========== -Extensions are the basic building blocks of Construct. They provide Actions, -Tasks, and Templates. All builtin actions and templates are provided by the -Builtins Extension. Extensions can provide Tasks to extend existing Actions or -provide their own Actions. - - -.. code-block:: python - - from construct import Extension - - class MyExtension(Extension): - - name = 'My Extension' - attr_name = 'my_extension' - - def load(self): - # setup extension - # add Actions, Tasks, and template paths - pass - - def available(self, ctx): - # Return True if Extension is available in ctx - return True - - -Actions -======= -Actions are interfaces to run a set of Tasks ordered by their respective -priorities and requirements. Let's take a look at the :class:`NewProject` Action provided by the Builtins Extension. - - -.. code-block:: python - - import construct - from construct import Action - - class NewProject(Action): - '''Create a new Project''' - - label = 'New Project' - identifier = 'new.project' - - @staticmethod - def parameters(ctx): - params = dict( - root={ - 'label': 'Project Root', - 'required': True, - 'type': types.String, - 'help': 'project root directory', - }, - template={ - 'label': 'Project Template', - 'required': True, - 'type': types.String, - 'help': 'name of a project template', - } - ) - - if not ctx: - return params - - templates = list(construct.get_templates('project').keys()) - params['template']['options'] = templates - if templates: - params['template']['default'] = templates[0] - - return params - - @staticmethod - def available(ctx): - return not ctx.project - - -Tasks -===== -Tasks are decorated python functions that are added to an Action and ordered by -their respective priorities and requirements. - -.. code-block:: python - - from construct.tasks import task, requires, success, returns, artifact - - @task - @requires(success('other_task')) - @returns(artifact('message')) - def some_task(): - return 'other_task completed successfully' - -This task is useless, but, it shows us a few neat things about Tasks. Our task -'some_task' requires the success of 'other_task' and returns an artifact named -'message'. Construct provides a rich set of decorators and functions to help -you setup the flow of your Actions. Tasks also remain callable after being -decorated, so you can run them on their own which is nice for testing. - -.. code-block:: python - - assert some_task() == 'other_task completed successfully' +Asset Libraries Are Projects Too +================================ +# TODO diff --git a/docs/html/_sources/configuration.rst.txt b/docs/html/_sources/configuration.rst.txt index bd85fa6..e59a866 100644 --- a/docs/html/_sources/configuration.rst.txt +++ b/docs/html/_sources/configuration.rst.txt @@ -1,149 +1,197 @@ ============= Configuration ============= +When starting up Construct will look for a construct.yaml file in the following locations: -Construct uses yaml files for configuration. Here is the default configuration, -construct/defaults/construct.yaml. You can specify your own configuration by -creating your own yaml file, and pointing your CONSTRUCT_CONFIG environment -variable to it. +1. Paths stored in the CONSTRUCT_PATH environment variable +2. Your USER_PATH ``~/.construct`` +The first construct.yaml that is found becomes your settings folder. If no construct.yaml file is found a default settings structure is created in the last path checked (default: `~/.construct`). The settings folder contains user icons, extensions, templates and software configurations. -.. code-block:: yaml +To share a configuration accross your stuiod set CONSTRUCT_PATH on your workstations to a network folder. You can also override the default lookup behavior by providing your own list of paths when initializing the Construct API. - ROOT: '~/projects' - EXTENSION_PATHS: - - '~/.construct' - EVENT_STREAM_DB: '~/.construct/eventstream.sqlite' - DISABLE_EXTENSIONS: [] +:: - LOGGING: + import construct + api = construct.API(path=['~/.custom_path']) + +.. note:: Settings files are validated prior to being loaded. This prevents Construct from running with faulty settings. + +Settings folder structure +========================= + +:: + + extensions/ + icons/ + templates/ + software/ + construct.yaml + +extensions folder +----------------- +Python modules and packages containing extensions folders. This is the location where Construct discovers and loads Extensions. + +icons folder +------------ +One of the locations where Construct will look for icons. When configuring software you can place custom +icons here and reference them using `icons/myicon.png`. + +templates folder +---------------- +Template files like a maya workspace.mel. These templates can be used in software configurations. + +software folder +--------------- +Individual software configuration files. + +construct.yaml +-------------- +The main settings file. By default it looks like this. +:: + + locations: + local: + lib: ~/lib + projects: ~/projects + my_location: local + my_mount: projects + extensions: [] + logging: version: 1 formatters: simple: - format: '%(levelname).1s:%(name)s> %(message)s' + format: '%(levelname).1s:%(name)s> %(message)s' handlers: console: - formatter: simple class: logging.StreamHandler + formatter: simple loggers: construct: + handlers: + - console level: DEBUG - handlers: [console] - propagate: False - - - STATUSES: - - 'waiting' - - 'in progress' - - 'rendering' - - 'pending' - - 'rejected' - - 'complete' - - - TASK_TYPES: - - model - - rig - - shade - - texture - - comp - - light - - layout - - - PATH_TEMPLATES: - asset_type: '{project}/assets/{asset_type}' - asset: '{project}/assets/{asset_type}/{asset}' - sequence: '{project}/sequences/{sequence}' - shot: '{project}/sequences/{sequence}/{shot}' - task: '{parent}/{task}' - publish: '{task}/publish/{file_type}/{task}_{name}/v{:0>3d}' - publish_file: '{task}_{name}_v{:0>3d}.{ext}' - workspace: '{task}/work/{workspace}' - workspace_file: '{task}_{name}_v{:0>3d}.{ext}' - - - SOFTWARE: - maya2017: - host: 'maya' - workspace: 'maya' - identifier: 'launch.maya2017' - description: 'Launch Autodesk Maya2017' - default_workspace: 'maya' - icon: './icons/maya.png' - cmd: - win: - path: C:/Program Files/Autodesk/Maya2017/bin/maya.exe - args: [] - linux: - path: /usr/autodesk/maya2017/bin/maya - args: [] - mac: - path: /Applications/Autodesk/maya2017/bin/maya - args: [] - maya2018: - host: 'maya' - identifier: 'launch.maya2018' - description: 'Launch Autodesk Maya2018' - default_workspace: 'maya' - icon: './icons/maya.png' - extensions: ['.ma', '.mb', '.fbx', '.obj', '.abc'] - cmd: - win: - path: C:/Program Files/Autodesk/Maya2018/bin/maya.exe - args: [] - linux: - path: /usr/autodesk/maya2018/bin/maya - args: [] - mac: - path: /Applications/Autodesk/maya2018/bin/maya - args: [] - nuke11.1v2: - host: 'nuke' - identifier: 'launch.nuke11.1v2' - description: 'Launch The Foundry Nuke11.1v2' - default_workspace: 'nuke' - icon: './icons/nuke.png' - extensions: ['.nk'] - cmd: - win: - path: C:/Program Files/Nuke11.1v2/Nuke11.1.exe - args: [] - linux: - path: /usr/local/Nuke11.1v2/Nuke11.1 - args: [] - mac: - path: /Applications/Nuke11.1v2/Nuke11.1v2.app/Nuke11.1 - args: [] - sublime: - host: 'sublime' - identifier: 'launch.sublimetext' - description: 'Launch Sublimt Text 3' - default_workspace: null - icon: './icons/sublime.png' - extensions: ['.txt', '.py', '.mel'] - cmd: - win: - path: C:/Program Files/Sublime Text 3//subl.exe - args: [] - linux: - path: /usr/bin/subl - args: [] - mac: - path: /Applications/Sublime Text.app/Contents/SharedSupport/bin/subl - args: [] - - - FILE_TYPES: - alembic: - extensions: ['.abc'] - maya: - extensions: ['.ma', '.mb'] - nuke: - extensions: ['.nk'] - c4d: - extensions: ['.c4d'] - photoshop: - extensions: ['.psd', '.psb'] - illustrator: - extensions: ['.ai'] + + +Builtin Settings +================ +The builtin settings that must be included in your construct.yaml file. + +locations +--------- +A dictionary containing locations and file system mounts. These are used to lookup file system paths to projects, folders, assets, and files. The default locations value supports a simple local setup. + +:: + + locations: + local: + lib: ~/lib + projects: ~/projects + +For a multisite solution you can define multiple locations and OS specific mounts. + +:: + + locations: + NY: + lib: + win: //ny-server/lib + mac: /Volumes/ny-server/lib + linux: /mnt/ny-server/lib + projects: + win: //ny-server/projects + mac: /Volumes/ny-server/projects + linux: /mnt/ny-server/projects + LA: + lib: + win: //la-server/lib + mac: /Volumes/la-server/lib + linux: /mnt/la-server/lib + projects: + win: //la-server/projects + mac: /Volumes/la-server/projects + linux: /mnt/la-server/projects + +my_location +----------- +The default location that Construct will use. + +Default: ``local`` + +my_mount +-------- +The default mount that Construct will use. + +Default: ``projects`` + +extensions +---------- +A list of extensions to import and load when Construct starts. This is where you would list extensions +installed using pip. + +.. note:: Builtin Extensions and Extensions located in your settings `extensions` folder will be loaded automatically, they do not need to be included here. + +default: ``[]`` + +logging +------- +A dictConfig used to configure logging. + + +Software Settings +================= +Software is configured in individual yaml files stored in the software settings folder. Let's take a look at how we would configure Autodesk Maya2019. + +software/maya2019.yaml:: + + label: Autodesk Maya 2019 + icon: icons/maya.png + host: maya + cmd: + linux: /usr/Autodesk/Maya2019/bin/maya + mac: /applications/Autodesk/Maya2019.app/bin/maya.exe + win: C:/Program Files/Autodesk/Maya2019/bin/maya.exe + args: [] + files: + templates/workspace.mel: workspace.mel + folders: + - data + - cache + extensions: ['.ma', '.mb', '.fbx', '.abc', '.obj'] + env: {} + +label +----- +The nice name for the software. + +icon +---- +The path to an icon relative to your settings folder. + +host +---- +The identifier of a ``Host`` extension that handles this software. Usually you will have to write your own ``Host`` extensions to support any meaningful integration with a piece of software. + +cmd +--- +OS specific paths to the software executables. + +args +---- +Args to pass to the software executable. + +files +----- +Files to copy to the workspace root when created. Each key is the path to a template file and it's accompanying value is the path relative to the workspace root where it will be copied to. + +folders +------- +A list of folders to create in the workspace root. + +extensions +---------- +A list of extensions that this software is able to open. + +env +--- +Environment variables to set when launching this software. When values are lists they will be prepended to your current Environment variables value. When values are strings they will override an existing Environment variable value. diff --git a/docs/html/_sources/environment.rst.txt b/docs/html/_sources/environment.rst.txt index 661a29c..5bd3bae 100644 --- a/docs/html/_sources/environment.rst.txt +++ b/docs/html/_sources/environment.rst.txt @@ -1,11 +1,3 @@ ===================== Environment Variables ===================== - - -CONSTRUCT_ROOT - Root directory containing your projects. Defaults to `~/projects`. - -CONSTRUCT_CONFIG - Path to your Construct config file. - If not set Construct will use `construct/defaults/construct.yaml`. diff --git a/docs/html/_sources/guide.rst.txt b/docs/html/_sources/guide.rst.txt index 8bf2f92..b2caeff 100644 --- a/docs/html/_sources/guide.rst.txt +++ b/docs/html/_sources/guide.rst.txt @@ -1,150 +1,20 @@ ========= API Guide ========= +# TODO -Initialize Construct -==================== -Before we can do anything useful with construct, we need to initialize it. +Quickstart +========== +# TODO -.. code-block:: python - - import construct - construct.init() - -The initialization process performs the following actions to configure -construct. - -1. loads default config or config file set by CONSTRUCT_CONFIG env var -2. Configures logging for the "construct" namespace -3. Builds Context from your environment variables -4. Discovers Extensions in the following locations - - - construct.extensions entry_point - - paths passed to construct.init - - paths set in EXTENSION_PATHS config key - - paths defined by CONSTRUCT_EXTENSION_PATHS env var - -5. Loads Builtins Extension -6. Configures fsfs - -Create a Project +Builtin Extensions ================== +# TODO -.. code-block:: python - - my_project = construct.new_project( - root='~/projects/my_project', - template='vfx_project' - ) - - -Create a Shot with an animation task -==================================== - -.. code-block:: python - - seq010 = construct.new_sequence( - project=my_project, - name='seq010', - template='vfx_sequence' - ) - sh010 = construct.new_shot( - sequence=seq010, - name='sh010', - template='vfx_shot' - ) - sh010_anim = construct.new_task( - parent=sh010, - type='animation' - ) - - -Create an Asset with a model task -================================= - -.. code-block:: python - - coffee_cup = construct.new_asset( - project=my_project, - asset_type='prop', - name='coffee_cup', - template='vfx_asset' - ) - coffee_cup_model = construct.new_task( - parent=coffee_cup, - type='model' - template='vfx_task' - ) - - -Working with Context -==================== -You can set the current context from a path... - -.. code-block:: python - - construct.set_context_from_path('~/projects/my_project') - -or from an Entry. - -.. code-block:: python - - my_project = construct.search('my_project').one() - construct.set_context_from_entry(my_project) - -Get the Actions available in your current context... - -.. code-block:: python - - available_actions = construct.actions.collect() - -or in a specific context. - -.. code-block:: python - - ctx = construct.Context.from_path('~/projects/other_project') - other_actions = construct.actions.collect(ctx) - - -Searching -========= - -Find all projects - -.. code-block:: python - - all_projects = construct.search(tags=['project']) - -Find one project - -.. code-block:: python - - project = construct.search('my_project', tags=['project']).one() - -Find all assets in a project - -.. code-block:: python - - assets = project.assets - - -Find all model tasks in a project - -.. code-block:: python - - model_tasks = project.children().tags('model', 'task') - - -Find all shots in a sequence - -.. code-block:: python - - seq = project.sequences.name('seq010').one() - shots = seq.shots - - -Find a nested Entry using a name selector - -.. code-block:: python +Extending Construct +=================== +# TODO - model_task = construct.search('my_project/coffee_cup/model') +Examples +======== +# TODO diff --git a/docs/html/_sources/index.rst.txt b/docs/html/_sources/index.rst.txt index ccb2155..ceb32bb 100644 --- a/docs/html/_sources/index.rst.txt +++ b/docs/html/_sources/index.rst.txt @@ -1,44 +1,24 @@ .. image:: https://img.shields.io/github/license/construct-org/construct.svg?style=flat-square - :target: https://github.com/danbradham/construct/blob/master/LICENSE + :target: https://github.com/construct-org/construct/blob/master/LICENSE :alt: License .. image:: https://img.shields.io/travis/construct-org/construct.svg?style=flat-square - :target: https://travis-ci.org/danbradham/construct + :target: https://travis-ci.org/construct-org/construct :alt: Travis ========= construct ========= -An extensible API for creative project and asset management. +An extensible API for creative projects and asset libraries. Features ======== - - Manage Projects on your file system + - Manage Projects and Asset Libraries + - Create your own Extensions and Actions - Integrate with your Creative Software - - Manage Workspaces and Publishes - - Folder and Path Templating (fsfs_ and Lucidity_) - - Contextual API - - Extensions, Actions, and Tasks - - -Core Packages -============= - -+----------------------+------------------------------------------------+ -| package | description | -+======================+================================================+ -| construct_cpenv_ | Cpenv Integration for env management | -+----------------------+------------------------------------------------+ -| construct_launcher_ | Application launcher | -+----------------------+------------------------------------------------+ -| construct_maya_ | Autodesk Maya integration | -+----------------------+------------------------------------------------+ -| construct_nuke_ | The Foundry Nuke integration | -+----------------------+------------------------------------------------+ -| construct_ui_ | Graphical user interface | -+----------------------+------------------------------------------------+ + - Manage Work files and Publish files Installation @@ -49,27 +29,22 @@ Installation $ pip install git+ssh://git@github.com/construct-org/construct.git +Contribute +========== + + - Fork this repository + - Create a new branch + - Make a pull request + + .. toctree:: :maxdepth: 2 - self concepts - configuration - environment - cli guide + configuration api -.. _construct_cli: https://github.com/construct-org/construct_cli -.. _construct_cpenv: https://github.com/construct-org/construct_cpenv -.. _construct_templates: https://github.com/construct-org/construct_templates -.. _construct_launcher: https://github.com/construct-org/construct_launcher -.. _construct_maya: https://github.com/construct-org/construct_maya -.. _construct_nuke: https://github.com/construct-org/construct_nuke -.. _construct_ui: https://github.com/construct-org/construct_ui -.. _fsfs: https://github.com/danbradham/fsfs -.. _Lucidity: https://gitlab.com/4degrees/lucidity - * :ref:`genindex` * :ref:`modindex` diff --git a/docs/html/_static/basic.css b/docs/html/_static/basic.css index 19ced10..0807176 100644 --- a/docs/html/_static/basic.css +++ b/docs/html/_static/basic.css @@ -4,7 +4,7 @@ * * Sphinx stylesheet -- basic theme. * - * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -81,6 +81,10 @@ div.sphinxsidebar input { font-size: 1em; } +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + div.sphinxsidebar #searchbox input[type="text"] { float: left; width: 80%; @@ -427,6 +431,13 @@ table.field-list td, table.field-list th { hyphens: manual; } +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist td { + vertical-align: top; +} + + /* -- other body styles ----------------------------------------------------- */ ol.arabic { diff --git a/docs/html/_static/css/badge_only.css b/docs/html/_static/css/badge_only.css index 6362912..3c33cef 100644 --- a/docs/html/_static/css/badge_only.css +++ b/docs/html/_static/css/badge_only.css @@ -1,2 +1 @@ -.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;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:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.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 .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #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}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.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}} -/*# sourceMappingURL=badge_only.css.map */ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../fonts/fontawesome-webfont.eot");src:url("../fonts/fontawesome-webfont.eot?#iefix") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff") format("woff"),url("../fonts/fontawesome-webfont.ttf") format("truetype"),url("../fonts/fontawesome-webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:""}.icon-book:before{content:""}.fa-caret-down:before{content:""}.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.icon-caret-up:before{content:""}.fa-caret-left:before{content:""}.icon-caret-left:before{content:""}.fa-caret-right:before{content:""}.icon-caret-right:before{content:""}.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:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.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:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #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 .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.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}} diff --git a/docs/html/_static/css/theme.css b/docs/html/_static/css/theme.css index c1631d8..aed8cef 100644 --- a/docs/html/_static/css/theme.css +++ b/docs/html/_static/css/theme.css @@ -1,5 +1,6 @@ -*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{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:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{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{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}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="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:0.2em 0;background:#ccc;color:#000;padding:0.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{html,body,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^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title: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 dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome +/* sphinx_rtd_theme version 0.4.3 | MIT license */ +/* Built 20190212 16:02 */ +*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{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:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,.rst-content code,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{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{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}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="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;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{html,body,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^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:.5cm}p,h2,.rst-content .toctree-wrapper p.caption,h3{orphans:3;widows:3}h2,.rst-content .toctree-wrapper p.caption,h3{page-break-after:avoid}}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title: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 dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content .code-block-caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{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?v=4.6.3");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.6.3") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.6.3") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.6.3") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.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 dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{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:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.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 dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.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 dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.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 dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.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 dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-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:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{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-remove:before,.fa-close: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-gear:before,.fa-cog: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 tt.download span:first-child:before,.rst-content code.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-rotate-right:before,.fa-repeat: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-photo:before,.fa-image: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,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title: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-warning:before,.fa-exclamation-triangle: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-gears:before,.fa-cogs: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-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars: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,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard: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-unlink:before,.fa-chain-broken: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.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.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-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw: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-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.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-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap: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-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-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-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-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-soccer-ball-o:before,.fa-futbol-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-shekel:before,.fa-sheqel:before,.fa-ils: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-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full: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-stop-o:before,.fa-hand-paper-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-tv:before,.fa-television: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-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language: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:""}.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,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.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 dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title: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 dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:0.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title: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 dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:0.5;-webkit-transition:opacity 0.05s ease-in;-moz-transition:opacity 0.05s ease-in;transition:opacity 0.05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title: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 dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.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.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.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,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.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 .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.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.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note 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.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a{color:#2980B9}.wy-alert p:last-child,.rst-content .note 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 .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;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,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all 0.3s ease-in;-moz-transition:all 0.3s ease-in;transition:all 0.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 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;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 0.1s linear;-moz-transition:all 0.1s linear;transition:all 0.1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.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:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{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:before,.wy-btn-group:after{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:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,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:solid 1px #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 input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{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{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;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;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{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 select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.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="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.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="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.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="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.35765%;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{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){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{margin-right:0}.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{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:0.3em;color:#666;vertical-align:middle;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="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-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 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}input[type="datetime-local"]{padding:.34375em .625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}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="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]: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 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][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:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{width:36px;height:12px;margin:12px 0;position:relative;border-radius:4px;background:#ccc;cursor:pointer;-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.wy-switch:before{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out}.wy-switch:after{content:"false";position:absolute;left:48px;display:block;font-size:12px;color:#ccc}.wy-switch.active{background:#1e8449}.wy-switch.active:before{left:24px;background:#27AE60}.wy-switch.active:after{content:"true"}.wy-switch.disabled,.wy-switch.active.disabled{cursor:not-allowed}.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="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.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="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #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:0.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0.3em;display:block}.wy-form label{margin-bottom:0.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:0.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{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%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.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],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils 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 tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;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%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;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}h1,h2,.rst-content .toctree-wrapper p.caption,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;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{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}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;color:#E74C3C;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.codeblock-example{border:1px solid #e1e4e5;border-bottom:none;padding:24px;padding-top:48px;font-weight:500;background:#fff;position:relative}.codeblock-example:after{content:"Example";position:absolute;top:0px;left:0px;background:#9B59B6;color:#fff;padding:6px 12px}.codeblock-example.prettyprint-example-only{border:1px solid #e1e4e5;margin-bottom:24px}.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight']{border:1px solid #e1e4e5;padding:0px;overflow-x:auto;background:#fff;margin:1px 0 24px 0}.codeblock div[class^='highlight'],pre.literal-block div[class^='highlight'],.rst-content .literal-block div[class^='highlight'],div[class^='highlight'] div[class^='highlight']{border:none;background:none;margin:0}div[class^='highlight'] td.code{width:100%}.linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:12px;line-height:1.5;color:#d9d9d9}div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:12px;line-height:1.5;display:block;overflow:auto;color:#404040}@media print{.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight'],div[class^='highlight'] pre{white-space:pre-wrap}}.hll{background-color:#ffc;margin:0 -12px;padding:0 12px;display:block}.c{color:#998;font-style:italic}.err{color:#a61717;background-color:#e3d2d2}.k{font-weight:bold}.o{font-weight:bold}.cm{color:#998;font-style:italic}.cp{color:#999;font-weight:bold}.c1{color:#998;font-style:italic}.cs{color:#999;font-weight:bold;font-style:italic}.gd{color:#000;background-color:#fdd}.gd .x{color:#000;background-color:#faa}.ge{font-style:italic}.gr{color:#a00}.gh{color:#999}.gi{color:#000;background-color:#dfd}.gi .x{color:#000;background-color:#afa}.go{color:#888}.gp{color:#555}.gs{font-weight:bold}.gu{color:purple;font-weight:bold}.gt{color:#a00}.kc{font-weight:bold}.kd{font-weight:bold}.kn{font-weight:bold}.kp{font-weight:bold}.kr{font-weight:bold}.kt{color:#458;font-weight:bold}.m{color:#099}.s{color:#d14}.n{color:#333}.na{color:teal}.nb{color:#0086b3}.nc{color:#458;font-weight:bold}.no{color:teal}.ni{color:purple}.ne{color:#900;font-weight:bold}.nf{color:#900;font-weight:bold}.nn{color:#555}.nt{color:navy}.nv{color:teal}.ow{font-weight:bold}.w{color:#bbb}.mf{color:#099}.mh{color:#099}.mi{color:#099}.mo{color:#099}.sb{color:#d14}.sc{color:#d14}.sd{color:#d14}.s2{color:#d14}.se{color:#d14}.sh{color:#d14}.si{color:#d14}.sx{color:#d14}.sr{color:#009926}.s1{color:#d14}.ss{color:#990073}.bp{color:#999}.vc{color:teal}.vg{color:teal}.vi{color:teal}.il{color:#099}.gc{color:#999;background-color:#EAF2F5}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #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{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin-bottom:0;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;color:#555;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:0.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:0.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:0.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em;border-top:none;border-bottom:none}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:0.9em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical .local-toc li ul{display:block}.wy-menu-vertical li ul li a{margin-bottom:0;color:#b3b3b3;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#b3b3b3}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover span.toctree-expand{color:#b3b3b3}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.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;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.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 auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:0.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.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 0.2s ease-in;-moz-transition:all 0.2s ease-in;transition:all 0.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:left repeat-y #fcfcfc;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);background-size:300px 1px}.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%;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:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.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,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:#999}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:Consolas,"Andale Mono WT","Andale Mono","Lucida Console","Lucida Sans Typewriter","DejaVu Sans Mono","Bitstream Vera Sans Mono","Liberation Mono","Nimbus Mono L",Monaco,"Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:#999}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@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-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{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: 1400px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;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:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.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 .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #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}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.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.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 img{max-width:100%;height:auto !important}.rst-content .highlight>pre{line-height:normal}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.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{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .line-block{margin-left:24px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto;display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink{display:none;visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after{visibility:visible;content:"";font-family:FontAwesome;display:inline-block}.rst-content h1:hover .headerlink,.rst-content h2:hover .headerlink,.rst-content .toctree-wrapper p.caption:hover .headerlink,.rst-content h3:hover .headerlink,.rst-content h4:hover .headerlink,.rst-content h5:hover .headerlink,.rst-content h6:hover .headerlink,.rst-content dl dt:hover .headerlink,.rst-content p.caption:hover .headerlink{display:inline-block}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:super;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:#999}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{color:#555}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none;padding-top:5px}.rst-content table.field-list td>strong{display:inline-block;margin-top:3px}.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;padding-left:0}.rst-content tt,.rst-content tt,.rst-content code{color:#000;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:inline-block;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{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:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}@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:"Inconsolata";font-style:normal;font-weight:400;src:local("Inconsolata"),local("Inconsolata-Regular"),url(../fonts/Inconsolata-Regular.ttf) format("truetype")}@font-face{font-family:"Inconsolata";font-style:normal;font-weight:700;src:local("Inconsolata Bold"),local("Inconsolata-Bold"),url(../fonts/Inconsolata-Bold.ttf) format("truetype")}@font-face{font-family:"Lato";font-style:normal;font-weight:400;src:local("Lato Regular"),local("Lato-Regular"),url(../fonts/Lato-Regular.ttf) format("truetype")}@font-face{font-family:"Lato";font-style:normal;font-weight:700;src:local("Lato Bold"),local("Lato-Bold"),url(../fonts/Lato-Bold.ttf) format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:local("Roboto Slab Regular"),local("RobotoSlab-Regular"),url(../fonts/RobotoSlab-Regular.ttf) format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:local("Roboto Slab Bold"),local("RobotoSlab-Bold"),url(../fonts/RobotoSlab-Bold.ttf) format("truetype")} -/*# sourceMappingURL=theme.css.map */ + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.7.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.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 dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon{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.3333333333em;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.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.wy-menu-vertical li span.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-left.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-left.toctree-expand,.rst-content .fa-pull-left.admonition-title,.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 dl dt .fa-pull-left.headerlink,.rst-content p.caption .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.rst-content code.download span.fa-pull-left:first-child,.fa-pull-left.icon{margin-right:.3em}.fa.fa-pull-right,.wy-menu-vertical li span.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a span.fa-pull-right.toctree-expand,.wy-menu-vertical li.current>a span.fa-pull-right.toctree-expand,.rst-content .fa-pull-right.admonition-title,.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 dl dt .fa-pull-right.headerlink,.rst-content p.caption .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.rst-content code.download span.fa-pull-right:first-child,.fa-pull-right.icon{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.wy-menu-vertical li span.pull-left.toctree-expand,.wy-menu-vertical li.on a span.pull-left.toctree-expand,.wy-menu-vertical li.current>a span.pull-left.toctree-expand,.rst-content .pull-left.admonition-title,.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 dl dt .pull-left.headerlink,.rst-content p.caption .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content .code-block-caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.rst-content code.download span.pull-left:first-child,.pull-left.icon{margin-right:.3em}.fa.pull-right,.wy-menu-vertical li span.pull-right.toctree-expand,.wy-menu-vertical li.on a span.pull-right.toctree-expand,.wy-menu-vertical li.current>a span.pull-right.toctree-expand,.rst-content .pull-right.admonition-title,.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 dl dt .pull-right.headerlink,.rst-content p.caption .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content .code-block-caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.rst-content code.download span.pull-right:first-child,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-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:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{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-remove:before,.fa-close: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-gear:before,.fa-cog: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 tt.download span:first-child:before,.rst-content code.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-rotate-right:before,.fa-repeat: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-photo:before,.fa-image: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,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title: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-warning:before,.fa-exclamation-triangle: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-gears:before,.fa-cogs: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-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars: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,.wy-dropdown .caret:before,.icon-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard: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-unlink:before,.fa-chain-broken: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.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.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-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw: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-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li span.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-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap: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-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-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-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-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-soccer-ball-o:before,.fa-futbol-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-shekel:before,.fa-sheqel:before,.fa-ils: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-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery:before,.fa-battery-full: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-stop-o:before,.fa-hand-paper-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-tv:before,.fa-television: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-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language: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-vcard:before,.fa-address-card:before{content:""}.fa-vcard-o:before,.fa-address-card-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:before,.fa-thermometer-full: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-bathtub:before,.fa-s15:before,.fa-bath: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,.wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,.rst-content .admonition-title,.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 dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink,.rst-content tt.download span:first-child,.rst-content code.download span:first-child,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li.on a span.toctree-expand:before,.wy-menu-vertical li.current>a span.toctree-expand:before,.rst-content .admonition-title: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 dl dt .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content .code-block-caption .headerlink:before,.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li a span.toctree-expand,.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .rst-content p.caption .headerlink,.rst-content p.caption a .headerlink,a .rst-content table>caption .headerlink,.rst-content table>caption a .headerlink,a .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption a .headerlink,a .rst-content tt.download span:first-child,.rst-content tt.download a span:first-child,a .rst-content code.download span:first-child,.rst-content code.download a span:first-child,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .btn span.toctree-expand,.btn .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .btn span.toctree-expand,.btn .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .btn span.toctree-expand,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .rst-content p.caption .headerlink,.rst-content p.caption .btn .headerlink,.btn .rst-content table>caption .headerlink,.rst-content table>caption .btn .headerlink,.btn .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .btn .headerlink,.btn .rst-content tt.download span:first-child,.rst-content tt.download .btn span:first-child,.btn .rst-content code.download span:first-child,.rst-content code.download .btn span:first-child,.btn .icon,.nav .fa,.nav .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .nav span.toctree-expand,.nav .wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.on a .nav span.toctree-expand,.nav .wy-menu-vertical li.current>a span.toctree-expand,.wy-menu-vertical li.current>a .nav span.toctree-expand,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .rst-content p.caption .headerlink,.rst-content p.caption .nav .headerlink,.nav .rst-content table>caption .headerlink,.rst-content table>caption .nav .headerlink,.nav .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .nav .headerlink,.nav .rst-content tt.download span:first-child,.rst-content tt.download .nav span:first-child,.nav .rst-content code.download span:first-child,.rst-content code.download .nav span:first-child,.nav .icon{display:inline}.btn .fa.fa-large,.btn .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .btn span.fa-large.toctree-expand,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .btn .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.btn .rst-content .code-block-caption .fa-large.headerlink,.rst-content .code-block-caption .btn .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .btn span.fa-large:first-child,.btn .rst-content code.download span.fa-large:first-child,.rst-content code.download .btn span.fa-large:first-child,.btn .fa-large.icon,.nav .fa.fa-large,.nav .wy-menu-vertical li span.fa-large.toctree-expand,.wy-menu-vertical li .nav span.fa-large.toctree-expand,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .rst-content p.caption .fa-large.headerlink,.rst-content p.caption .nav .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.nav .rst-content .code-block-caption .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.nav .rst-content code.download span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.nav .fa-large.icon{line-height:.9em}.btn .fa.fa-spin,.btn .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .btn span.fa-spin.toctree-expand,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .btn .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.btn .rst-content .code-block-caption .fa-spin.headerlink,.rst-content .code-block-caption .btn .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .btn span.fa-spin:first-child,.btn .rst-content code.download span.fa-spin:first-child,.rst-content code.download .btn span.fa-spin:first-child,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .wy-menu-vertical li span.fa-spin.toctree-expand,.wy-menu-vertical li .nav span.fa-spin.toctree-expand,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .rst-content p.caption .fa-spin.headerlink,.rst-content p.caption .nav .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.nav .rst-content .code-block-caption .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.nav .rst-content code.download span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.wy-menu-vertical li span.btn.toctree-expand:before,.rst-content .btn.admonition-title: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 dl dt .btn.headerlink:before,.rst-content p.caption .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.rst-content code.download span.btn:first-child:before,.btn.icon:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.wy-menu-vertical li span.btn.toctree-expand:hover:before,.rst-content .btn.admonition-title: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 dl dt .btn.headerlink:hover:before,.rst-content p.caption .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.rst-content code.download span.btn:first-child:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .wy-menu-vertical li span.toctree-expand:before,.wy-menu-vertical li .btn-mini span.toctree-expand:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .rst-content p.caption .headerlink:before,.rst-content p.caption .btn-mini .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.rst-content tt.download .btn-mini span:first-child:before,.btn-mini .rst-content code.download span:first-child:before,.rst-content code.download .btn-mini span:first-child:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.rst-content .admonition-todo,.rst-content .admonition{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.admonition{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.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.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso,.rst-content .admonition-todo,.rst-content .wy-alert-warning.admonition{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .admonition-todo .admonition-title,.rst-content .wy-alert-warning.admonition .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.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,.rst-content .seealso,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.admonition{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.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 .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.admonition{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.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.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.admonition{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note 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.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a{color:#2980B9}.wy-alert p:last-child,.rst-content .note 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 .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child,.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;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,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 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27AE60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;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:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:visited{color:#fff}.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-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{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:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{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:before,.wy-btn-group:after{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:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,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:solid 1px #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 input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{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{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 .3125em 0;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;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{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 select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.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="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.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="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.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="color"]{width:100%}.wy-control-group .wy-form-full{float:left;display:block;margin-right:2.3576515979%;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.3576515979%;width:48.821174201%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.3576515979%;width:31.7615656014%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.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{margin:6px 0 0 0;font-size:90%}.wy-control-no-input{display:inline-block;margin:6px 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;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="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-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"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:.3125em;*height:13px;*width:13px}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="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129FEA}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#E74C3C;border:1px solid #E74C3C}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#E74C3C}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]: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}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type="radio"][disabled],input[type="checkbox"][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:solid 1px #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{position:absolute;content:"";display:block;left:0;top:0;width:36px;height:12px;border-radius:4px;background:#ccc;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{position:absolute;content:"";display:block;width:18px;height:18px;border-radius:4px;background:#999;left:-3px;top:-3px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.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="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.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="color"]{border:solid 1px #E74C3C}.wy-control-group.wy-control-group-error textarea{border:solid 1px #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="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:.3em;display:block}.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{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 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{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%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px}.wy-table td p:last-child,.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child{margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.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],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils 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 tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;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%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;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}h1,h2,.rst-content .toctree-wrapper p.caption,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;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2,.rst-content .toctree-wrapper p.caption{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}code,.rst-content tt,.rst-content code{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #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}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li p:last-child,.rst-content .section ul li p:last-child,.rst-content .toctree-wrapper ul li p:last-child,article ul li p:last-child{margin-bottom:0}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-disc li ol li,.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,article ul li ol li{list-style:decimal}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.wy-plain-list-decimal li p:last-child,.rst-content .section ol li p:last-child,.rst-content ol.arabic li p:last-child,article ol li p:last-child{margin-bottom:0}.wy-plain-list-decimal li ul,.rst-content .section ol li ul,.rst-content ol.arabic li ul,article ol li ul{margin-bottom:0}.wy-plain-list-decimal li ul li,.rst-content .section ol li ul li,.rst-content ol.arabic li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:before,.wy-breadcrumbs:after{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs li code,.wy-breadcrumbs li .rst-content tt,.rst-content .wy-breadcrumbs li tt{padding:5px;border:none;background:none}.wy-breadcrumbs li code.literal,.wy-breadcrumbs li .rst-content tt.literal,.rst-content .wy-breadcrumbs li tt.literal{color:#404040}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.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:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #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:#3a7ca8;height:32px;display:inline-block;line-height:32px;padding:0 1.618em;margin:12px 0 0 0;display:block;font-weight:bold;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li code,.wy-menu-vertical li .rst-content tt,.rst-content .wy-menu-vertical li tt{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li span.toctree-expand{display:block;float:left;margin-left:-1.2em;font-size:.8em;line-height:1.6em;color:#4d4d4d}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.on a:hover span.toctree-expand,.wy-menu-vertical li.current>a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.on a span.toctree-expand,.wy-menu-vertical li.current>a span.toctree-expand{display:block;font-size:.8em;line-height:1.6em;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a{color:#404040}.wy-menu-vertical li.toctree-l1.current li.toctree-l2>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>ul{display:none}.wy-menu-vertical li.toctree-l1.current li.toctree-l2.current>ul,.wy-menu-vertical li.toctree-l2.current li.toctree-l3.current>ul{display:block}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{display:block;background:#c9c9c9;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l2 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l2 span.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3{font-size:.9em}.wy-menu-vertical li.toctree-l3.current>a{background:#bdbdbd;padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{display:block;background:#bdbdbd;padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l3 a:hover span.toctree-expand{color:gray}.wy-menu-vertical li.toctree-l3 span.toctree-expand{color:#969696}.wy-menu-vertical li.toctree-l4{font-size:.9em}.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:normal}.wy-menu-vertical a{display:inline-block;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 span.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980B9;cursor:pointer;color:#fff}.wy-menu-vertical a:active span.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;padding:.809em;display:block;color:#fcfcfc;margin-bottom:.809em}.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 auto;height:45px;width:45px;background-color:#2980B9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-side-nav-search>a img.logo,.wy-side-nav-search .wy-dropdown>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search>a.icon img.logo,.wy-side-nav-search .wy-dropdown>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:normal;color:rgba(255,255,255,0.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:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.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,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:gray}footer p{margin-bottom:12px}footer span.commit code,footer span.commit .rst-content tt,.rst-content footer span.commit tt{padding:0px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:1em;background:none;border:none;color:gray}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{width:100%}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:before,.rst-breadcrumbs-buttons:after{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;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-side-scroll{width:auto}.wy-side-nav-search{width:auto}.wy-menu.wy-menu-vertical{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,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,footer,.wy-nav-side{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:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .wy-menu-vertical li span.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version span.toctree-expand,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content p.caption .headerlink,.rst-content p.caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.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:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #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 .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.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.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 img{max-width:100%;height:auto}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure p.caption{font-style:italic}.rst-content div.figure p:last-child.caption{margin-bottom:0px}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img,.rst-content .section>a>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"";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 12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;display:block;overflow:auto}.rst-content pre.literal-block,.rst-content div[class^='highlight']{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px 0}.rst-content pre.literal-block div[class^='highlight'],.rst-content div[class^='highlight'] div[class^='highlight']{padding:0px;border:none;margin:0}.rst-content div[class^='highlight'] td.code{width:100%}.rst-content .linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 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 12px;display:block;overflow:auto}.rst-content div[class^='highlight'] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content pre.literal-block,.rst-content div[class^='highlight'] pre,.rst-content .linenodiv pre{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;font-size:12px;line-height:1.4}.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 .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last,.rst-content .admonition-todo .last,.rst-content .admonition .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.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{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .section ol p:last-child,.rst-content .section ul p:last-child{margin-bottom:24px}.rst-content .line-block{margin-left:0px;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content .toctree-wrapper p.caption .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.rst-content p.caption .headerlink,.rst-content table>caption .headerlink,.rst-content .code-block-caption .headerlink{visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content .toctree-wrapper p.caption .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after,.rst-content p.caption .headerlink:after,.rst-content table>caption .headerlink:after,.rst-content .code-block-caption .headerlink:after{content:"";font-family:FontAwesome}.rst-content h1:hover .headerlink:after,.rst-content h2:hover .headerlink:after,.rst-content .toctree-wrapper p.caption:hover .headerlink:after,.rst-content h3:hover .headerlink:after,.rst-content h4:hover .headerlink:after,.rst-content h5:hover .headerlink:after,.rst-content h6:hover .headerlink:after,.rst-content dl dt:hover .headerlink:after,.rst-content p.caption:hover .headerlink:after,.rst-content table>caption:hover .headerlink:after,.rst-content .code-block-caption:hover .headerlink:after{visibility:visible}.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:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#F1C40F;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:baseline;position:relative;top:-0.4em;line-height:0;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:gray}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.docutils.citation tt,.rst-content table.docutils.citation code,.rst-content table.docutils.footnote tt,.rst-content table.docutils.footnote code{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}.rst-content table.docutils td .last,.rst-content table.docutils td .last :last-child{margin-bottom:0}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list td p{font-size:inherit;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 tt,.rst-content tt,.rst-content code{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace;padding:2px 5px}.rst-content tt big,.rst-content tt em,.rst-content tt big,.rst-content code big,.rst-content tt em,.rst-content code em{font-size:100% !important;line-height:normal}.rst-content tt.literal,.rst-content tt.literal,.rst-content code.literal{color:#E74C3C}.rst-content tt.xref,a .rst-content tt,.rst-content tt.xref,.rst-content code.xref,a .rst-content tt,a .rst-content code{font-weight:bold;color:#404040}.rst-content pre,.rst-content kbd,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace}.rst-content a tt,.rst-content a tt,.rst-content a code{color:#2980B9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold;margin-bottom:12px}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980B9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:#555}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) tt,.rst-content dl:not(.docutils) code{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname,.rst-content dl:not(.docutils) tt.descclassname,.rst-content dl:not(.docutils) code.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) code.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{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:bold}.rst-content tt.download,.rst-content code.download{background:inherit;padding:inherit;font-weight:normal;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content tt.download span:first-child,.rst-content code.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content tt.download span:first-child:before,.rst-content code.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.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/lato-regular.eot");src:url("../fonts/Lato/lato-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-regular.woff2") format("woff2"),url("../fonts/Lato/lato-regular.woff") format("woff"),url("../fonts/Lato/lato-regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bold.eot");src:url("../fonts/Lato/lato-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bold.woff2") format("woff2"),url("../fonts/Lato/lato-bold.woff") format("woff"),url("../fonts/Lato/lato-bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-bolditalic.eot");src:url("../fonts/Lato/lato-bolditalic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-bolditalic.woff2") format("woff2"),url("../fonts/Lato/lato-bolditalic.woff") format("woff"),url("../fonts/Lato/lato-bolditalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:"Lato";src:url("../fonts/Lato/lato-italic.eot");src:url("../fonts/Lato/lato-italic.eot?#iefix") format("embedded-opentype"),url("../fonts/Lato/lato-italic.woff2") format("woff2"),url("../fonts/Lato/lato-italic.woff") format("woff"),url("../fonts/Lato/lato-italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:400;src:url("../fonts/RobotoSlab/roboto-slab.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-regular.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-regular.ttf") format("truetype")}@font-face{font-family:"Roboto Slab";font-style:normal;font-weight:700;src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot");src:url("../fonts/RobotoSlab/roboto-slab-v7-bold.eot?#iefix") format("embedded-opentype"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff2") format("woff2"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.woff") format("woff"),url("../fonts/RobotoSlab/roboto-slab-v7-bold.ttf") format("truetype")} diff --git a/docs/html/_static/doctools.js b/docs/html/_static/doctools.js index 0c15c00..344db17 100644 --- a/docs/html/_static/doctools.js +++ b/docs/html/_static/doctools.js @@ -4,7 +4,7 @@ * * Sphinx JavaScript utilities for all documentation. * - * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ @@ -70,7 +70,9 @@ jQuery.fn.highlightText = function(text, className) { if (node.nodeType === 3) { var val = node.nodeValue; var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { var span; var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); if (isInSVG) { @@ -148,7 +150,9 @@ var Documentation = { this.fixFirefoxAnchorBug(); this.highlightSearchWords(); this.initIndexTable(); - + if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { + this.initOnKeyListeners(); + } }, /** @@ -308,4 +312,4 @@ _ = Documentation.gettext; $(document).ready(function() { Documentation.init(); -}); \ No newline at end of file +}); diff --git a/docs/html/_static/documentation_options.js b/docs/html/_static/documentation_options.js index 93b72f2..ee43ef4 100644 --- a/docs/html/_static/documentation_options.js +++ b/docs/html/_static/documentation_options.js @@ -1,9 +1,10 @@ var DOCUMENTATION_OPTIONS = { - URL_ROOT: '', - VERSION: '0.1.7', + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '0.2.0', LANGUAGE: 'None', COLLAPSE_INDEX: false, FILE_SUFFIX: '.html', HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt' + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, }; \ No newline at end of file diff --git a/docs/html/_static/fonts/Inconsolata.ttf b/docs/html/_static/fonts/Inconsolata.ttf new file mode 100644 index 0000000..4b8a36d Binary files /dev/null and b/docs/html/_static/fonts/Inconsolata.ttf differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.eot b/docs/html/_static/fonts/Lato/lato-bold.eot new file mode 100644 index 0000000..3361183 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bold.eot differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.ttf b/docs/html/_static/fonts/Lato/lato-bold.ttf new file mode 100644 index 0000000..29f691d Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bold.ttf differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.woff b/docs/html/_static/fonts/Lato/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bold.woff differ diff --git a/docs/html/_static/fonts/Lato/lato-bold.woff2 b/docs/html/_static/fonts/Lato/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bold.woff2 differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.eot b/docs/html/_static/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 0000000..3d41549 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bolditalic.eot differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.ttf b/docs/html/_static/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 0000000..f402040 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bolditalic.ttf differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.woff b/docs/html/_static/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bolditalic.woff differ diff --git a/docs/html/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/html/_static/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.eot b/docs/html/_static/fonts/Lato/lato-italic.eot new file mode 100644 index 0000000..3f82642 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-italic.eot differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.ttf b/docs/html/_static/fonts/Lato/lato-italic.ttf new file mode 100644 index 0000000..b4bfc9b Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-italic.ttf differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.woff b/docs/html/_static/fonts/Lato/lato-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-italic.woff differ diff --git a/docs/html/_static/fonts/Lato/lato-italic.woff2 b/docs/html/_static/fonts/Lato/lato-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-italic.woff2 differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.eot b/docs/html/_static/fonts/Lato/lato-regular.eot new file mode 100644 index 0000000..11e3f2a Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-regular.eot differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.ttf b/docs/html/_static/fonts/Lato/lato-regular.ttf new file mode 100644 index 0000000..74decd9 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-regular.ttf differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.woff b/docs/html/_static/fonts/Lato/lato-regular.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-regular.woff differ diff --git a/docs/html/_static/fonts/Lato/lato-regular.woff2 b/docs/html/_static/fonts/Lato/lato-regular.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/docs/html/_static/fonts/Lato/lato-regular.woff2 differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 0000000..79dc8ef Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 0000000..df5d1df Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 0000000..2f7ca78 Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 0000000..eb52a79 Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/docs/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.eot b/docs/html/_static/fonts/fontawesome-webfont.eot index c7b00d2..e9f60ca 100644 Binary files a/docs/html/_static/fonts/fontawesome-webfont.eot and b/docs/html/_static/fonts/fontawesome-webfont.eot differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.svg b/docs/html/_static/fonts/fontawesome-webfont.svg index 8b66187..855c845 100644 --- a/docs/html/_static/fonts/fontawesome-webfont.svg +++ b/docs/html/_static/fonts/fontawesome-webfont.svg @@ -1,685 +1,2671 @@ - - + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/html/_static/fonts/fontawesome-webfont.ttf b/docs/html/_static/fonts/fontawesome-webfont.ttf index f221e50..35acda2 100644 Binary files a/docs/html/_static/fonts/fontawesome-webfont.ttf and b/docs/html/_static/fonts/fontawesome-webfont.ttf differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.woff b/docs/html/_static/fonts/fontawesome-webfont.woff index 6e7483c..400014a 100644 Binary files a/docs/html/_static/fonts/fontawesome-webfont.woff and b/docs/html/_static/fonts/fontawesome-webfont.woff differ diff --git a/docs/html/_static/fonts/fontawesome-webfont.woff2 b/docs/html/_static/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/docs/html/_static/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/html/_static/js/theme.js b/docs/html/_static/js/theme.js index af661a9..8555d79 100644 --- a/docs/html/_static/js/theme.js +++ b/docs/html/_static/js/theme.js @@ -1,169 +1,3 @@ -require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o"); - - // Add expand links to all parents of nested ul - $('.wy-menu-vertical ul').not('.simple').siblings('a').each(function () { - var link = $(this); - expand = $(''); - expand.on('click', function (ev) { - self.toggleCurrent(link); - ev.stopPropagation(); - return false; - }); - link.prepend(expand); - }); - }; - - nav.reset = function () { - // Get anchor from URL and open up nested nav - var anchor = encodeURI(window.location.hash); - if (anchor) { - try { - var link = $('.wy-menu-vertical') - .find('[href="' + anchor + '"]'); - // If we didn't find a link, it may be because we clicked on - // something that is not in the sidebar (eg: when using - // sphinxcontrib.httpdomain it generates headerlinks but those - // aren't picked up and placed in the toctree). So let's find - // the closest header in the document and try with that one. - if (link.length === 0) { - var doc_link = $('.document a[href="' + anchor + '"]'); - var closest_section = doc_link.closest('div.section'); - // Try again with the closest section entry. - link = $('.wy-menu-vertical') - .find('[href="#' + closest_section.attr("id") + '"]'); - - } - $('.wy-menu-vertical li.toctree-l1 li.current') - .removeClass('current'); - link.closest('li.toctree-l2').addClass('current'); - link.closest('li.toctree-l3').addClass('current'); - link.closest('li.toctree-l4').addClass('current'); - } - catch (err) { - console.log("Error expanding nav for anchor", err); - } - } - }; - - nav.onScroll = function () { - this.winScroll = false; - var newWinPosition = this.win.scrollTop(), - winBottom = newWinPosition + this.winHeight, - navPosition = this.navBar.scrollTop(), - newNavPosition = navPosition + (newWinPosition - this.winPosition); - if (newWinPosition < 0 || winBottom > this.docHeight) { - return; - } - this.navBar.scrollTop(newNavPosition); - this.winPosition = newWinPosition; - }; - - nav.onResize = function () { - this.winResize = false; - this.winHeight = this.win.height(); - this.docHeight = $(document).height(); - }; - - nav.hashChange = function () { - this.linkScroll = true; - this.win.one('hashchange', function () { - this.linkScroll = false; - }); - }; - - nav.toggleCurrent = function (elem) { - var parent_li = elem.closest('li'); - parent_li.siblings('li.current').removeClass('current'); - parent_li.siblings().find('li.current').removeClass('current'); - parent_li.find('> ul li.current').removeClass('current'); - parent_li.toggleClass('current'); - } - - return nav; -}; - -module.exports.ThemeNav = ThemeNav(); - -if (typeof(window) != 'undefined') { - window.SphinxRtdTheme = { StickyNav: module.exports.ThemeNav }; -} - -},{"jquery":"jquery"}]},{},["sphinx-rtd-theme"]); +/* sphinx_rtd_theme version 0.4.3 | MIT license */ +/* Built 20190212 16:02 */ +require=function r(s,a,l){function c(e,n){if(!a[e]){if(!s[e]){var i="function"==typeof require&&require;if(!n&&i)return i(e,!0);if(u)return u(e,!0);var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}var o=a[e]={exports:{}};s[e][0].call(o.exports,function(n){return c(s[e][1][n]||n)},o,o.exports,r,s,a,l)}return a[e].exports}for(var u="function"==typeof require&&require,n=0;n"),i("table.docutils.footnote").wrap("
"),i("table.docutils.citation").wrap("
"),i(".wy-menu-vertical ul").not(".simple").siblings("a").each(function(){var e=i(this);expand=i(''),expand.on("click",function(n){return t.toggleCurrent(e),n.stopPropagation(),!1}),e.prepend(expand)})},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),i=e.find('[href="'+n+'"]');if(0===i.length){var t=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(i=e.find('[href="#'+t.attr("id")+'"]')).length&&(i=e.find('[href="#"]'))}0this.docHeight||(this.navBar.scrollTop(i),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",function(){this.linkScroll=!1})},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current"),e.siblings().find("li.current").removeClass("current"),e.find("> ul li.current").removeClass("current"),e.toggleClass("current")}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:e.exports.ThemeNav,StickyNav:e.exports.ThemeNav}),function(){for(var r=0,n=["ms","moz","webkit","o"],e=0;e0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + + + + + +var splitChars = (function() { + var result = {}; + var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, + 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702, + 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971, + 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345, + 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761, + 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823, + 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125, + 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695, + 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587, + 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141]; + var i, j, start, end; + for (i = 0; i < singles.length; i++) { + result[singles[i]] = true; + } + var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709], + [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161], + [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568], + [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807], + [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047], + [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383], + [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450], + [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547], + [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673], + [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820], + [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946], + [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023], + [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173], + [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332], + [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481], + [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718], + [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791], + [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095], + [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205], + [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687], + [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968], + [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869], + [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102], + [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271], + [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592], + [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822], + [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167], + [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959], + [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143], + [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318], + [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483], + [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101], + [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567], + [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292], + [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444], + [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783], + [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311], + [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511], + [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774], + [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071], + [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263], + [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519], + [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647], + [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967], + [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295], + [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274], + [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007], + [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381], + [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]]; + for (i = 0; i < ranges.length; i++) { + start = ranges[i][0]; + end = ranges[i][1]; + for (j = start; j <= end; j++) { + result[j] = true; + } + } + return result; +})(); + +function splitQuery(query) { + var result = []; + var start = -1; + for (var i = 0; i < query.length; i++) { + if (splitChars[query.charCodeAt(i)]) { + if (start !== -1) { + result.push(query.slice(start, i)); + start = -1; + } + } else if (start === -1) { + start = i; + } + } + if (start !== -1) { + result.push(query.slice(start)); + } + return result; +} + + diff --git a/docs/html/_static/searchtools.js b/docs/html/_static/searchtools.js index 41b8336..5ff3180 100644 --- a/docs/html/_static/searchtools.js +++ b/docs/html/_static/searchtools.js @@ -1,331 +1,52 @@ /* - * searchtools.js_t + * searchtools.js * ~~~~~~~~~~~~~~~~ * * Sphinx JavaScript utilities for the full-text search. * - * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ - -/* Non-minified version JS is _stemmer.js if file is provided */ -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' +if (!Scorer) { + /** + * Simple result scoring code. + */ + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [filename, title, anchor, descr, score] + // and returns the new score. + /* + score: function(result) { + return result[4]; + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: {0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5}, // used to be unimportantResults + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + // query found in terms + term: 5 }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } } - - -/** - * Simple result scoring code. - */ -var Scorer = { - // Implement the following function to further tweak the score for each result - // The function takes a result array [filename, title, anchor, descr, score] - // and returns the new score. - /* - score: function(result) { - return result[4]; - }, - */ - - // query matches the full name of an object - objNameMatch: 11, - // or matches in the last dotted part of the object name - objPartialMatch: 6, - // Additive scores depending on the priority of the object - objPrio: {0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5}, // used to be unimportantResults - // Used when the priority is not in the mapping. - objPrioDefault: 0, - - // query found in title - title: 15, - // query found in terms - term: 5 -}; - - - - - -var splitChars = (function() { - var result = {}; - var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, - 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702, - 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971, - 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345, - 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761, - 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823, - 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125, - 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695, - 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587, - 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141]; - var i, j, start, end; - for (i = 0; i < singles.length; i++) { - result[singles[i]] = true; - } - var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709], - [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161], - [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568], - [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807], - [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047], - [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383], - [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450], - [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547], - [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673], - [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820], - [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946], - [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023], - [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173], - [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332], - [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481], - [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718], - [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791], - [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095], - [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205], - [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687], - [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968], - [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869], - [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102], - [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271], - [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592], - [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822], - [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167], - [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959], - [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143], - [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318], - [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483], - [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101], - [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567], - [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292], - [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444], - [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783], - [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311], - [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511], - [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774], - [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071], - [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263], - [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519], - [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647], - [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967], - [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295], - [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274], - [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007], - [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381], - [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]]; - for (i = 0; i < ranges.length; i++) { - start = ranges[i][0]; - end = ranges[i][1]; - for (j = start; j <= end; j++) { - result[j] = true; - } - } - return result; -})(); - -function splitQuery(query) { - var result = []; - var start = -1; - for (var i = 0; i < query.length; i++) { - if (splitChars[query.charCodeAt(i)]) { - if (start !== -1) { - result.push(query.slice(start, i)); - start = -1; - } - } else if (start === -1) { - start = i; - } - } - if (start !== -1) { - result.push(query.slice(start)); - } - return result; +if (!splitQuery) { + function splitQuery(query) { + return query.split(/\s+/); + } } - - - /** * Search Module */ @@ -417,7 +138,6 @@ var Search = { */ query : function(query) { var i; - var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; // stem the searchterms and add them to the correct list var stemmer = new Stemmer(); @@ -758,4 +478,4 @@ var Search = { $(document).ready(function() { Search.init(); -}); \ No newline at end of file +}); diff --git a/docs/html/_static/websupport.js b/docs/html/_static/websupport.js index 78e14bb..3b4999e 100644 --- a/docs/html/_static/websupport.js +++ b/docs/html/_static/websupport.js @@ -4,7 +4,7 @@ * * sphinx.websupport utilities for all documentation. * - * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. + * :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ diff --git a/docs/html/api.html b/docs/html/api.html index 10d134d..48ccce4 100644 --- a/docs/html/api.html +++ b/docs/html/api.html @@ -8,7 +8,7 @@ - API Documentation — construct 0.1.7 documentation + API Documentation — construct 0.2.0 documentation @@ -17,41 +17,35 @@ - - + + + + + + + + - - - - - - - - - - - - - - + - - + + + + + - +
-